From 60c2591bd2c00b36c9040ec42867ec7e47fd0f00 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 10:18:52 -0600 Subject: [PATCH 001/191] ' -> ` --- token-lending/program/src/instruction.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index aa4d9ed8b2d..979ad993314 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -34,7 +34,7 @@ pub enum LendingInstruction { /// 0. `[writable]` Lending market account. /// 1. `[]` Quote currency SPL Token mint. Must be initialized. /// 2. `[]` Rent sysvar - /// 3. '[]` Token program id + /// 3. `[]` Token program id InitLendingMarket { /// Owner authority which can add new reserves market_owner: Pubkey, @@ -60,7 +60,7 @@ pub enum LendingInstruction { /// 11 `[]` User transfer authority ($authority). /// 12 `[]` Clock sysvar /// 13 `[]` Rent sysvar - /// 14 '[]` Token program id + /// 14 `[]` Token program id /// 15 `[optional]` Serum DEX market account. Not required for quote currency reserves. Must be initialized and match quote and base currency. InitReserve { /// Initial amount of liquidity to deposit into the new reserve @@ -84,7 +84,7 @@ pub enum LendingInstruction { /// 7. `[]` Derived lending market authority. /// 8. `[]` Clock sysvar /// 9. `[]` Rent sysvar - /// 10 '[]` Token program id + /// 10 `[]` Token program id InitObligation, // 3 @@ -102,7 +102,7 @@ pub enum LendingInstruction { /// 6. `[]` Derived lending market authority. /// 7. `[signer]` User transfer authority ($authority). /// 8. `[]` Clock sysvar - /// 9. '[]` Token program id + /// 9. `[]` Token program id DepositReserveLiquidity { /// Amount to deposit into the reserve liquidity_amount: u64, @@ -122,7 +122,7 @@ pub enum LendingInstruction { /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. /// 7. `[signer]` User transfer authority ($authority). - /// 8. '[]` Token program id + /// 8. `[]` Token program id WithdrawReserveLiquidity { /// Amount of collateral to deposit in exchange for liquidity collateral_amount: u64, @@ -153,7 +153,7 @@ pub enum LendingInstruction { /// 14 `[]` Dex market order book side /// 15 `[]` Temporary memory /// 16 `[]` Clock sysvar - /// 17 '[]` Token program id + /// 17 `[]` Token program id /// 18 `[optional, writable]` Deposit reserve collateral host fee receiver account. BorrowReserveLiquidity { // TODO: slippage constraint @@ -240,7 +240,7 @@ pub enum LendingInstruction { /// 6. `[]` Lending market account. /// 7. `[]` Derived lending market authority. /// 8. `[signer]` User transfer authority ($authority). - /// 9. '[]` Token program id + /// 9. `[]` Token program id DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, @@ -266,7 +266,7 @@ pub enum LendingInstruction { /// 11 `[]` Dex market order book side /// 12 `[]` Temporary memory /// 13 `[]` Clock sysvar - /// 14 '[]` Token program id + /// 14 `[]` Token program id WithdrawObligationCollateral { /// Amount of collateral to withdraw collateral_amount: u64, From b1723d3b720d1df9f22a0981f755fa4fc99a785c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 11:27:07 -0600 Subject: [PATCH 002/191] updated instructions --- token-lending/program/src/instruction.rs | 159 ++++++++++++++--------- 1 file changed, 97 insertions(+), 62 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 979ad993314..c950524cf11 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -74,17 +74,15 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[]` Deposit reserve account. - /// 1. `[]` Borrow reserve account. - /// 2. `[writable]` Obligation - /// 3. `[writable]` Obligation token mint - /// 4. `[writable]` Obligation token output - /// 5. `[]` Obligation token owner - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[]` Clock sysvar - /// 9. `[]` Rent sysvar - /// 10 `[]` Token program id + /// 0. `[writable]` Obligation + /// 1. `[writable]` Obligation token mint + /// 2. `[writable]` Obligation token output + /// 3. `[]` Obligation token owner + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[]` Clock sysvar + /// 7. `[]` Rent sysvar + /// 8. `[]` Token program id InitObligation, // 3 @@ -130,32 +128,25 @@ pub enum LendingInstruction { // 5 /// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens - /// is calculated by market price. The debt obligation is tokenized. + /// is calculated by market price. The debt obligation is tokenized. Requires a recently + /// refreshed obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source collateral token account, minted by deposit reserve collateral mint, - /// $authority can transfer $collateral_amount + /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account /// 1. `[writable]` Destination liquidity token account, minted by borrow reserve liquidity mint - /// 2. `[]` Deposit reserve account. - /// 3. `[writable]` Deposit reserve collateral supply SPL Token account - /// 4. `[writable]` Deposit reserve collateral fee receiver account. + /// 2. `[writable]` Borrow reserve account. + /// 3. `[writable]` Borrow reserve liquidity fee receiver account. /// Must be the fee account specified at InitReserve. - /// 5. `[writable]` Borrow reserve account. - /// 6. `[writable]` Borrow reserve liquidity supply SPL Token account - /// 7. `[writable]` Obligation - /// 8. `[writable]` Obligation token mint - /// 9. `[writable]` Obligation token output - /// 10 `[]` Lending market account. - /// 11 `[]` Derived lending market authority. - /// 12 `[signer]` User transfer authority ($authority). - /// 13 `[]` Dex market - /// 14 `[]` Dex market order book side - /// 15 `[]` Temporary memory - /// 16 `[]` Clock sysvar - /// 17 `[]` Token program id - /// 18 `[optional, writable]` Deposit reserve collateral host fee receiver account. - BorrowReserveLiquidity { + /// 4. `[writable]` Obligation + /// 5. `[writable]` Obligation liquidity + /// 6. `[]` Lending market account. + /// 7. `[]` Derived lending market authority. + /// 8. `[signer]` User transfer authority ($authority). + /// 9. `[]` Clock sysvar + /// 10 `[]` Token program id + /// 11 `[optional, writable]` Borrow reserve liquidity host fee receiver account. + BorrowObligationLiquidity { // TODO: slippage constraint /// Amount whose usage depends on `amount_type` amount: u64, @@ -164,33 +155,30 @@ pub enum LendingInstruction { }, // 6 - /// Repay loaned tokens to a reserve and receive collateral tokens. The obligation balance - /// will be recalculated for interest. + /// Repay loaned tokens to a reserve. The obligation balance will be recalculated for interest. + /// Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Source liquidity token account, minted by repay reserve liquidity mint - /// $authority can transfer $collateral_amount - /// 1. `[writable]` Destination collateral token account, minted by withdraw reserve collateral mint + /// $authority can transfer $liquidity_amount + /// 3. `[writable]` Destination repay reserve liquidity supply SPL Token account /// 2. `[writable]` Repay reserve account. - /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[]` Withdraw reserve account. - /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation - initialized - /// 7. `[writable]` Obligation token mint - /// 8. `[writable]` Obligation token input, $authority can transfer calculated amount + /// 6. `[writable]` Obligation + /// 6. `[writable]` Obligation liquidity /// 9. `[]` Lending market account. /// 10 `[]` Derived lending market authority. /// 11 `[signer]` User transfer authority ($authority). /// 12 `[]` Clock sysvar /// 13 `[]` Token program id - RepayReserveLiquidity { - /// Amount of loan to repay + RepayObligationLiquidity { + /// Amount of liquidity to repay liquidity_amount: u64, }, // 7 /// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy. + /// Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// @@ -201,12 +189,12 @@ pub enum LendingInstruction { /// 3. `[writable]` Repay reserve liquidity supply SPL Token account /// 4. `[]` Withdraw reserve account. /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation - initialized + /// 6. `[writable]` Obligation + /// 6. `[writable]` Obligation collateral + /// 6. `[writable]` Obligation liquidity /// 7. `[]` Lending market account. /// 8. `[]` Derived lending market authority. /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Dex market - /// 11 `[]` Dex market order book side /// 12 `[]` Temporary memory /// 13 `[]` Clock sysvar /// 14 `[]` Token program id @@ -226,7 +214,7 @@ pub enum LendingInstruction { AccrueReserveInterest, // 9 - /// Deposit additional collateral to an obligation. + /// Deposit additional collateral to an obligation. Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// @@ -235,19 +223,22 @@ pub enum LendingInstruction { /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account /// 2. `[]` Deposit reserve account. /// 3. `[writable]` Obligation - /// 4. `[writable]` Obligation token mint - /// 5. `[writable]` Obligation token output - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` User transfer authority ($authority). - /// 9. `[]` Token program id + /// 4. `[writable]` Obligation collateral + /// 5. `[writable]` Obligation token mint + /// 6. `[writable]` Obligation token output + /// 7. `[]` Lending market account. + /// 8. `[]` Derived lending market authority. + /// 9. `[signer]` User transfer authority ($authority). + /// 10 `[]` Clock sysvar + /// 11 `[]` Token program id DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, }, // 10 - /// Withdraw excess collateral from an obligation. The loan must remain healthy. + /// Withdraw excess collateral from an obligation. The loan must remain healthy. Requires a + /// recently refreshed obligation. /// /// Accounts expected by this instruction: /// @@ -255,18 +246,15 @@ pub enum LendingInstruction { /// 1. `[writable]` Destination collateral token account, minted by withdraw reserve /// collateral mint. $authority can transfer $collateral_amount /// 2. `[]` Withdraw reserve account. - /// 3. `[]` Borrow reserve account. - /// 4. `[writable]` Obligation + /// 3. `[writable]` Obligation + /// 4. `[writable]` Obligation collateral /// 5. `[writable]` Obligation token mint /// 6. `[writable]` Obligation token input /// 7. `[]` Lending market account. /// 8. `[]` Derived lending market authority. /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Dex market - /// 11 `[]` Dex market order book side - /// 12 `[]` Temporary memory - /// 13 `[]` Clock sysvar - /// 14 `[]` Token program id + /// 10 `[]` Clock sysvar + /// 11 `[]` Token program id WithdrawObligationCollateral { /// Amount of collateral to withdraw collateral_amount: u64, @@ -283,6 +271,53 @@ pub enum LendingInstruction { /// The new owner new_owner: Pubkey, }, + + // 12 + /// Refresh market value of an obligation's collateral. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation collateral account + /// 1. `[]` Deposit reserve account + /// 2. `[]` Dex market + /// 3. `[]` Dex market order book side + /// 4. `[]` Lending market account + /// 5. `[]` Derived lending market authority + /// 6. `[]` Temporary memory + /// 7. `[]` Clock sysvar + /// 8. `[]` Token program id + RefreshObligationCollateral, + + // 13 + /// Refresh market value and accrued interest of an obligation's liquidity. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation liquidity account + /// 1. `[]` Borrow reserve account + /// 2. `[]` Dex market + /// 3. `[]` Dex market order book side + /// 4. `[]` Lending market account + /// 5. `[]` Derived lending market authority + /// 6. `[]` Temporary memory + /// 7. `[]` Clock sysvar + /// 8. `[]` Token program id + RefreshObligationLiquidity, + + // 14 + /// Refresh an obligation's loan to value ratio. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation account + /// 1..1+NUM_COL `[]` Obligation collateral accounts + /// 2+NUM_COL..2+NUM_LIQ `[]` Obligation liquidity accounts + /// 3+NUM_COL+NUM_LIQ `[]` Lending market account + /// 4+NUM_COL+NUM_LIQ `[]` Derived lending market authority + /// 5+NUM_COL+NUM_LIQ `[]` Temporary memory + /// 6+NUM_COL+NUM_LIQ `[]` Clock sysvar + /// 7+NUM_COL+NUM_LIQ `[]` Token program id + RefreshObligation, } impl LendingInstruction { From a1f0f3a08fe1ce68d5a8687f5381b752a4ab861c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 11:40:52 -0600 Subject: [PATCH 003/191] rename instruction and function refs --- token-lending/program/src/instruction.rs | 32 +++++++++++++++------- token-lending/program/src/processor.rs | 28 +++++++++---------- token-lending/program/src/state/reserve.rs | 2 +- token-lending/program/tests/helpers/mod.rs | 4 +-- token-lending/program/tests/repay.rs | 4 +-- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index c950524cf11..566be4885ee 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -373,14 +373,14 @@ impl LendingInstruction { let (amount_type, _rest) = Self::unpack_u8(rest)?; let amount_type = BorrowAmountType::from_u8(amount_type) .ok_or(LendingError::InstructionUnpackError)?; - Self::BorrowReserveLiquidity { + Self::BorrowObligationLiquidity { amount, amount_type, } } 6 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; - Self::RepayReserveLiquidity { liquidity_amount } + Self::RepayObligationLiquidity { liquidity_amount } } 7 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; @@ -399,6 +399,9 @@ impl LendingInstruction { let (new_owner, _rest) = Self::unpack_pubkey(rest)?; Self::SetLendingMarketOwner { new_owner } } + 12 => Self::RefreshObligationCollateral, + 13 => Self::RefreshObligationLiquidity, + 14 => Self::RefreshObligation, _ => return Err(LendingError::InstructionUnpackError.into()), }) } @@ -490,7 +493,7 @@ impl LendingInstruction { buf.push(4); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::BorrowReserveLiquidity { + Self::BorrowObligationLiquidity { amount, amount_type, } => { @@ -498,7 +501,7 @@ impl LendingInstruction { buf.extend_from_slice(&amount.to_le_bytes()); buf.extend_from_slice(&amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RepayReserveLiquidity { liquidity_amount } => { + Self::RepayObligationLiquidity { liquidity_amount } => { buf.push(6); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } @@ -521,6 +524,15 @@ impl LendingInstruction { buf.push(11); buf.extend_from_slice(new_owner.as_ref()); } + Self::RefreshObligationCollateral => { + buf.push(12); + } + Self::RefreshObligationLiquidity => { + buf.push(13); + } + Self::RefreshObligation => { + buf.push(14); + } } buf } @@ -700,9 +712,9 @@ pub fn withdraw_reserve_liquidity( } } -/// Creates a 'BorrowReserveLiquidity' instruction. +/// Creates a 'BorrowObligationLiquidity' instruction. #[allow(clippy::too_many_arguments)] -pub fn borrow_reserve_liquidity( +pub fn borrow_obligation_liquidity( program_id: Pubkey, amount: u64, amount_type: BorrowAmountType, @@ -753,7 +765,7 @@ pub fn borrow_reserve_liquidity( Instruction { program_id, accounts, - data: LendingInstruction::BorrowReserveLiquidity { + data: LendingInstruction::BorrowObligationLiquidity { amount, amount_type, } @@ -761,9 +773,9 @@ pub fn borrow_reserve_liquidity( } } -/// Creates a `RepayReserveLiquidity` instruction +/// Creates a `RepayObligationLiquidity` instruction #[allow(clippy::too_many_arguments)] -pub fn repay_reserve_liquidity( +pub fn repay_obligation_liquidity( program_id: Pubkey, liquidity_amount: u64, source_liquidity_pubkey: Pubkey, @@ -797,7 +809,7 @@ pub fn repay_reserve_liquidity( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RepayReserveLiquidity { liquidity_amount }.pack(), + data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 292ad1ec6c6..7a504c7b6d4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -51,30 +51,30 @@ pub fn process_instruction( } LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { msg!("Instruction: Deposit"); - process_deposit(program_id, liquidity_amount, accounts) + process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts) } LendingInstruction::WithdrawReserveLiquidity { collateral_amount } => { msg!("Instruction: Withdraw"); - process_withdraw(program_id, collateral_amount, accounts) + process_withdraw_reserve_liquidity(program_id, collateral_amount, accounts) } - LendingInstruction::BorrowReserveLiquidity { + LendingInstruction::BorrowObligationLiquidity { amount, amount_type, } => { msg!("Instruction: Borrow"); - process_borrow(program_id, amount, amount_type, accounts) + process_borrow_obligation_liquidity(program_id, amount, amount_type, accounts) } - LendingInstruction::RepayReserveLiquidity { liquidity_amount } => { + LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { msg!("Instruction: Repay"); - process_repay(program_id, liquidity_amount, accounts) + process_repay_obligation_liquidity(program_id, liquidity_amount, accounts) } LendingInstruction::LiquidateObligation { liquidity_amount } => { msg!("Instruction: Liquidate"); - process_liquidate(program_id, liquidity_amount, accounts) + process_liquidate_obligation(program_id, liquidity_amount, accounts) } LendingInstruction::AccrueReserveInterest => { msg!("Instruction: Accrue Interest"); - process_accrue_interest(program_id, accounts) + process_accrue_reserve_interest(program_id, accounts) } LendingInstruction::DepositObligationCollateral { collateral_amount } => { msg!("Instruction: Deposit Obligation Collateral"); @@ -423,7 +423,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro Ok(()) } -fn process_deposit( +fn process_deposit_reserve_liquidity( program_id: &Pubkey, liquidity_amount: u64, accounts: &[AccountInfo], @@ -512,7 +512,7 @@ fn process_deposit( Ok(()) } -fn process_withdraw( +fn process_withdraw_reserve_liquidity( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], @@ -602,7 +602,7 @@ fn process_withdraw( } #[inline(never)] // avoid stack frame limit -fn process_borrow( +fn process_borrow_obligation_liquidity( program_id: &Pubkey, token_amount: u64, token_amount_type: BorrowAmountType, @@ -840,7 +840,7 @@ fn process_borrow( } #[inline(never)] // avoid stack frame limit -fn process_repay( +fn process_repay_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, accounts: &[AccountInfo], @@ -996,7 +996,7 @@ fn process_repay( } #[inline(never)] // avoid stack frame limit -fn process_liquidate( +fn process_liquidate_obligation( program_id: &Pubkey, liquidity_amount: u64, accounts: &[AccountInfo], @@ -1174,7 +1174,7 @@ fn process_liquidate( } #[inline(never)] // avoid stack frame limit -fn process_accrue_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { +fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; for reserve_info in account_info_iter { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 9affeb713fa..3e5faf2d073 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -550,7 +550,7 @@ pub struct ReserveConfig { /// of collateral token amounts during repayments and liquidations. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct ReserveFees { - /// Fee assessed on `BorrowReserveLiquidity`, expressed as a Wad. + /// 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 diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 6d2339af81e..bbe7d844f47 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -15,7 +15,7 @@ use spl_token::{ }; use spl_token_lending::{ instruction::{ - borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_obligation, + borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, init_obligation, init_reserve, liquidate_obligation, BorrowAmountType, }, math::{Decimal, Rate, TryAdd, TryMul}, @@ -662,7 +662,7 @@ impl TestLendingMarket { 65548, &spl_token_lending::id(), ), - borrow_reserve_liquidity( + borrow_obligation_liquidity( spl_token_lending::id(), amount, borrow_amount_type, diff --git a/token-lending/program/tests/repay.rs b/token-lending/program/tests/repay.rs index 8a9f01062e4..def32709d76 100644 --- a/token-lending/program/tests/repay.rs +++ b/token-lending/program/tests/repay.rs @@ -11,7 +11,7 @@ use solana_sdk::{ }; use spl_token::instruction::approve; use spl_token_lending::{ - instruction::repay_reserve_liquidity, + instruction::repay_obligation_liquidity, math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, processor::process_instruction, state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, @@ -112,7 +112,7 @@ async fn test_success() { OBLIGATION_COLLATERAL, ) .unwrap(), - repay_reserve_liquidity( + repay_obligation_liquidity( spl_token_lending::id(), OBLIGATION_LOAN, usdc_reserve.user_liquidity_account, From a399999ceeabb86dbe76977fd73dfac141416b5e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 12:39:21 -0600 Subject: [PATCH 004/191] origination fees come out of liquidity now --- token-lending/client/src/main.rs | 8 +- token-lending/program/src/instruction.rs | 8 +- token-lending/program/src/processor.rs | 30 ++++---- token-lending/program/src/state/reserve.rs | 35 ++++----- token-lending/program/tests/borrow.rs | 20 +++-- token-lending/program/tests/helpers/mod.rs | 84 ++++++++++----------- token-lending/program/tests/init_reserve.rs | 2 +- 7 files changed, 96 insertions(+), 91 deletions(-) diff --git a/token-lending/client/src/main.rs b/token-lending/client/src/main.rs index 50b807b8936..78fa1ca0bfb 100644 --- a/token-lending/client/src/main.rs +++ b/token-lending/client/src/main.rs @@ -180,8 +180,8 @@ pub fn create_reserve( let reserve_pubkey = reserve_keypair.pubkey(); let collateral_mint_keypair = Keypair::new(); let collateral_supply_keypair = Keypair::new(); - let collateral_fees_receiver_keypair = Keypair::new(); let liquidity_supply_keypair = Keypair::new(); + let liquidity_fees_receiver_keypair = Keypair::new(); let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority = Keypair::new(); @@ -214,14 +214,14 @@ pub fn create_reserve( ), create_account( &payer.pubkey(), - &collateral_fees_receiver_keypair.pubkey(), + &liquidity_supply_keypair.pubkey(), token_balance, Token::LEN as u64, &spl_token::id(), ), create_account( &payer.pubkey(), - &liquidity_supply_keypair.pubkey(), + &liquidity_fees_receiver_keypair.pubkey(), token_balance, Token::LEN as u64, &spl_token::id(), @@ -282,7 +282,7 @@ pub fn create_reserve( liquidity_supply_keypair.pubkey(), collateral_mint_keypair.pubkey(), collateral_supply_keypair.pubkey(), - collateral_fees_receiver_keypair.pubkey(), + liquidity_fees_receiver_keypair.pubkey(), lending_market_pubkey, lending_market_owner.pubkey(), user_transfer_authority.pubkey(), diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 566be4885ee..821348804d1 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -571,9 +571,9 @@ pub fn init_reserve( reserve_pubkey: Pubkey, reserve_liquidity_mint_pubkey: Pubkey, reserve_liquidity_supply_pubkey: Pubkey, + reserve_liquidity_fees_receiver_pubkey: Pubkey, reserve_collateral_mint_pubkey: Pubkey, reserve_collateral_supply_pubkey: Pubkey, - reserve_collateral_fees_receiver_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, @@ -587,9 +587,9 @@ pub fn init_reserve( AccountMeta::new(reserve_pubkey, false), AccountMeta::new_readonly(reserve_liquidity_mint_pubkey, false), AccountMeta::new(reserve_liquidity_supply_pubkey, false), + AccountMeta::new(reserve_liquidity_fees_receiver_pubkey, false), AccountMeta::new(reserve_collateral_mint_pubkey, false), AccountMeta::new(reserve_collateral_supply_pubkey, false), - AccountMeta::new(reserve_collateral_fees_receiver_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_owner_pubkey, true), AccountMeta::new_readonly(lending_market_authority_pubkey, false), @@ -722,9 +722,9 @@ pub fn borrow_obligation_liquidity( destination_liquidity_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, deposit_reserve_collateral_supply_pubkey: Pubkey, - deposit_reserve_collateral_fees_receiver_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, borrow_reserve_liquidity_supply_pubkey: Pubkey, + borrow_reserve_liquidity_fees_receiver_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, @@ -741,9 +741,9 @@ pub fn borrow_obligation_liquidity( AccountMeta::new(destination_liquidity_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new(deposit_reserve_collateral_supply_pubkey, false), - AccountMeta::new(deposit_reserve_collateral_fees_receiver_pubkey, false), AccountMeta::new(borrow_reserve_pubkey, false), AccountMeta::new(borrow_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(borrow_reserve_liquidity_fees_receiver_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new(obligation_token_mint_pubkey, false), AccountMeta::new(obligation_token_output_pubkey, false), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7a504c7b6d4..c6edf786b09 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -174,9 +174,9 @@ fn process_init_reserve( let reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_mint_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fees_receiver_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_collateral_supply_info = next_account_info(account_info_iter)?; - let reserve_collateral_fees_receiver_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_owner_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; @@ -249,11 +249,11 @@ fn process_init_reserve( *reserve_liquidity_mint_info.key, reserve_liquidity_mint.decimals, *reserve_liquidity_supply_info.key, + *reserve_liquidity_fees_receiver_info.key, ); let reserve_collateral_info = ReserveCollateral::new( *reserve_collateral_mint_info.key, *reserve_collateral_supply_info.key, - *reserve_collateral_fees_receiver_info.key, ); let mut reserve = Reserve::new(NewReserveParams { current_slot: clock.slot, @@ -283,17 +283,17 @@ fn process_init_reserve( })?; spl_token_init_account(TokenInitializeAccountParams { - account: reserve_collateral_supply_info.clone(), - mint: reserve_collateral_mint_info.clone(), - owner: lending_market_authority_info.clone(), + account: reserve_liquidity_fees_receiver_info.clone(), + mint: reserve_liquidity_mint_info.clone(), + owner: lending_market_owner_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; spl_token_init_account(TokenInitializeAccountParams { - account: reserve_collateral_fees_receiver_info.clone(), + account: reserve_collateral_supply_info.clone(), mint: reserve_collateral_mint_info.clone(), - owner: lending_market_owner_info.clone(), + owner: lending_market_authority_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; @@ -617,9 +617,9 @@ fn process_borrow_obligation_liquidity( let destination_liquidity_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; let deposit_reserve_collateral_supply_info = next_account_info(account_info_iter)?; - let deposit_reserve_collateral_fees_receiver_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let borrow_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let borrow_reserve_liquidity_fees_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_output_info = next_account_info(account_info_iter)?; @@ -683,16 +683,16 @@ fn process_borrow_obligation_liquidity( msg!("Cannot use deposit reserve collateral supply as source account input"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.fees_receiver - != deposit_reserve_collateral_fees_receiver_info.key - { - msg!("Invalid deposit reserve collateral fees receiver account"); - return Err(LendingError::InvalidAccountInput.into()); - } if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { msg!("Cannot use borrow reserve liquidity supply as destination account input"); return Err(LendingError::InvalidAccountInput.into()); } + if &borrow_reserve.liquidity.fees_receiver + != borrow_reserve_liquidity_fees_receiver_info.key + { + msg!("Invalid borrow reserve liquidity fees receiver account"); + return Err(LendingError::InvalidAccountInput.into()); + } // TODO: handle case when neither reserve is the quote currency if borrow_reserve.dex_market.is_none() && deposit_reserve.dex_market.is_none() { @@ -808,7 +808,7 @@ fn process_borrow_obligation_liquidity( if owner_fee > 0 { spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), - destination: deposit_reserve_collateral_fees_receiver_info.clone(), + destination: borrow_reserve_liquidity_fees_receiver_info.clone(), amount: owner_fee, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3e5faf2d073..200725e1d4b 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -213,8 +213,9 @@ impl Reserve { }; let (origination_fee, host_fee) = - self.config.fees.calculate_borrow_fees(collateral_amount)?; + self.config.fees.calculate_borrow_fees(borrow_amount)?; + // @FIXME: fees collateral_amount = collateral_amount .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; @@ -385,6 +386,8 @@ pub struct ReserveLiquidity { pub mint_decimals: u8, /// Reserve liquidity supply address pub supply_pubkey: Pubkey, + /// Reserve liquidity fees receiver address + pub fees_receiver: Pubkey, /// Reserve liquidity available pub available_amount: u64, /// Reserve liquidity borrowed @@ -393,11 +396,12 @@ pub struct ReserveLiquidity { impl ReserveLiquidity { /// New reserve liquidity info - pub fn new(mint_pubkey: Pubkey, mint_decimals: u8, supply_pubkey: Pubkey) -> Self { + pub fn new(mint_pubkey: Pubkey, mint_decimals: u8, supply_pubkey: Pubkey, fees_receiver: Pubkey) -> Self { Self { mint_pubkey, mint_decimals, supply_pubkey, + fees_receiver, available_amount: 0, borrowed_amount_wads: Decimal::zero(), } @@ -451,17 +455,14 @@ pub struct ReserveCollateral { pub mint_total_supply: u64, /// Reserve collateral supply address pub supply_pubkey: Pubkey, - /// Reserve collateral fees receiver address - pub fees_receiver: Pubkey, } impl ReserveCollateral { /// New reserve collateral info - pub fn new(mint_pubkey: Pubkey, supply_pubkey: Pubkey, fees_receiver: Pubkey) -> Self { + pub fn new(mint_pubkey: Pubkey, supply_pubkey: Pubkey) -> Self { Self { mint_pubkey, supply_pubkey, - fees_receiver, ..Self::default() } } @@ -545,9 +546,9 @@ pub struct ReserveConfig { /// 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 collateral token amounts during repayments and liquidations. +/// 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)] pub struct ReserveFees { /// Fee assessed on `BorrowObligationLiquidity`, expressed as a Wad. @@ -565,11 +566,11 @@ impl ReserveFees { /// Calculate the owner and host fees on borrow pub fn calculate_borrow_fees( &self, - collateral_amount: u64, + borrow_amount: u64, ) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); - if borrow_fee_rate > Rate::zero() && collateral_amount > 0 { + if borrow_fee_rate > Rate::zero() && borrow_amount > 0 { let need_to_assess_host_fee = host_fee_rate > Rate::zero(); let minimum_fee = if need_to_assess_host_fee { 2 // 1 token to owner, 1 to host @@ -578,7 +579,7 @@ impl ReserveFees { }; let borrow_fee = borrow_fee_rate - .try_mul(collateral_amount)? + .try_mul(borrow_amount)? .try_round_u64()? .max(minimum_fee); @@ -588,7 +589,7 @@ impl ReserveFees { 0 }; - if borrow_fee >= collateral_amount { + if borrow_fee >= borrow_amount { Err(LendingError::BorrowTooSmall.into()) } else { Ok((borrow_fee, host_fee)) @@ -621,9 +622,9 @@ impl Pack for Reserve { liquidity_mint, liquidity_mint_decimals, liquidity_supply, + liquidity_fees_receiver, collateral_mint, collateral_supply, - collateral_fees_receiver, dex_market, optimal_utilization_rate, loan_to_value_ratio, @@ -653,6 +654,7 @@ impl Pack for Reserve { mint_pubkey: Pubkey::new_from_array(*liquidity_mint), mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), supply_pubkey: Pubkey::new_from_array(*liquidity_supply), + fees_receiver: Pubkey::new_from_array(*liquidity_fees_receiver), available_amount: u64::from_le_bytes(*available_liquidity), borrowed_amount_wads: unpack_decimal(total_borrows), }, @@ -660,7 +662,6 @@ impl Pack for Reserve { mint_pubkey: Pubkey::new_from_array(*collateral_mint), mint_total_supply: u64::from_le_bytes(*collateral_mint_supply), supply_pubkey: Pubkey::new_from_array(*collateral_supply), - fees_receiver: Pubkey::new_from_array(*collateral_fees_receiver), }, config: ReserveConfig { optimal_utilization_rate: u8::from_le_bytes(*optimal_utilization_rate), @@ -687,9 +688,9 @@ impl Pack for Reserve { liquidity_mint, liquidity_mint_decimals, liquidity_supply, + liquidity_fees_receiver, collateral_mint, collateral_supply, - collateral_fees_receiver, dex_market, optimal_utilization_rate, loan_to_value_ratio, @@ -719,13 +720,13 @@ impl Pack for Reserve { liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); + liquidity_fees_receiver.copy_from_slice(self.collateral.fees_receiver.as_ref()); *available_liquidity = self.liquidity.available_amount.to_le_bytes(); pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); // collateral info collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); - collateral_fees_receiver.copy_from_slice(self.collateral.fees_receiver.as_ref()); *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); // config diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index 90158d94221..8d497045755 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -116,10 +116,11 @@ async fn test_borrow_quote_currency() { let borrow_fees = TEST_RESERVE_CONFIG .fees - .calculate_borrow_fees(collateral_deposit_amount) + .calculate_borrow_fees(borrow_amount) .unwrap() .0; + // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); @@ -151,21 +152,22 @@ async fn test_borrow_quote_currency() { let collateral_deposited = 2 * collateral_deposit_amount; let (total_fee, host_fee) = TEST_RESERVE_CONFIG .fees - .calculate_borrow_fees(collateral_deposited) + .calculate_borrow_fees(borrow_amount) .unwrap(); assert!(total_fee > 0); assert!(host_fee > 0); + // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposited - total_fee); let fee_balance = - get_token_balance(&mut banks_client, sol_reserve.collateral_fees_receiver).await; + get_token_balance(&mut banks_client, sol_reserve.liquidity_fees_receiver).await; assert_eq!(fee_balance, total_fee - host_fee); - let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_host).await; + let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.liquidity_host).await; assert_eq!(host_fee_balance, host_fee); } @@ -272,10 +274,11 @@ async fn test_borrow_base_currency() { let borrow_fees = TEST_RESERVE_CONFIG .fees - .calculate_borrow_fees(collateral_deposit_amount) + .calculate_borrow_fees(borrow_amount) .unwrap() .0; + // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); @@ -302,7 +305,7 @@ async fn test_borrow_base_currency() { let (mut total_fee, mut host_fee) = TEST_RESERVE_CONFIG .fees - .calculate_borrow_fees(collateral_deposit_amount) + .calculate_borrow_fees(borrow_amount) .unwrap(); // avoid rounding error by assessing fees individually @@ -312,14 +315,15 @@ async fn test_borrow_base_currency() { assert!(total_fee > 0); assert!(host_fee > 0); + // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); let fee_balance = - get_token_balance(&mut banks_client, usdc_reserve.collateral_fees_receiver).await; + get_token_balance(&mut banks_client, usdc_reserve.liquidity_fees_receiver).await; assert_eq!(fee_balance, total_fee - host_fee); - let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.collateral_host).await; + let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.liquidity_host).await; assert_eq!(host_fee_balance, host_fee); } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index bbe7d844f47..2256efce43f 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -288,27 +288,34 @@ pub fn add_reserve( &spl_token::id(), ); - let collateral_fees_receiver_pubkey = Pubkey::new_unique(); + let amount = if let COption::Some(rent_reserve) = is_native { + liquidity_amount + rent_reserve + } else { + u32::MAX as u64 + }; + + let liquidity_supply_pubkey = Pubkey::new_unique(); test.add_packable_account( - collateral_fees_receiver_pubkey, - u32::MAX as u64, + liquidity_supply_pubkey, + amount, &Token { - mint: collateral_mint_pubkey, - owner: lending_market.owner.pubkey(), - amount: fees_amount, + mint: liquidity_mint_pubkey, + owner: lending_market.authority, + amount: liquidity_amount, state: AccountState::Initialized, + is_native, ..Token::default() }, &spl_token::id(), ); - let collateral_host_pubkey = Pubkey::new_unique(); + let liquidity_fees_receiver_pubkey = Pubkey::new_unique(); test.add_packable_account( - collateral_host_pubkey, + liquidity_fees_receiver_pubkey, u32::MAX as u64, &Token { - mint: collateral_mint_pubkey, - owner: user_accounts_owner.pubkey(), + mint: liquidity_mint_pubkey, + owner: lending_market.owner.pubkey(), amount: fees_amount, state: AccountState::Initialized, ..Token::default() @@ -316,22 +323,15 @@ pub fn add_reserve( &spl_token::id(), ); - let amount = if let COption::Some(rent_reserve) = is_native { - liquidity_amount + rent_reserve - } else { - u32::MAX as u64 - }; - - let liquidity_supply_pubkey = Pubkey::new_unique(); + let liquidity_host_pubkey = Pubkey::new_unique(); test.add_packable_account( - liquidity_supply_pubkey, - amount, + liquidity_host_pubkey, + u32::MAX as u64, &Token { mint: liquidity_mint_pubkey, - owner: lending_market.authority, - amount: liquidity_amount, + owner: user_accounts_owner.pubkey(), + amount: fees_amount, state: AccountState::Initialized, - is_native, ..Token::default() }, &spl_token::id(), @@ -343,11 +343,11 @@ pub fn add_reserve( liquidity_mint_pubkey, liquidity_mint_decimals, liquidity_supply_pubkey, + liquidity_fees_receiver_pubkey, ); let reserve_collateral = ReserveCollateral::new( collateral_mint_pubkey, collateral_supply_pubkey, - collateral_fees_receiver_pubkey, ); let mut reserve = Reserve::new(NewReserveParams { // intentionally wrapped to simulate elapsed slots @@ -413,10 +413,10 @@ pub fn add_reserve( liquidity_mint: liquidity_mint_pubkey, liquidity_mint_decimals, liquidity_supply: liquidity_supply_pubkey, + liquidity_fees_receiver: liquidity_fees_receiver_pubkey, + liquidity_host: liquidity_host_pubkey, collateral_mint: collateral_mint_pubkey, collateral_supply: collateral_supply_pubkey, - collateral_fees_receiver: collateral_fees_receiver_pubkey, - collateral_host: collateral_host_pubkey, user_liquidity_account: user_liquidity_pubkey, user_collateral_account: user_collateral_pubkey, dex_market: dex_market_pubkey, @@ -670,7 +670,7 @@ impl TestLendingMarket { borrow_reserve.user_liquidity_account, deposit_reserve.pubkey, deposit_reserve.collateral_supply, - deposit_reserve.collateral_fees_receiver, + deposit_reserve.liquidity_fees_receiver, borrow_reserve.pubkey, borrow_reserve.liquidity_supply, self.pubkey, @@ -682,7 +682,7 @@ impl TestLendingMarket { dex_market.pubkey, dex_market_orders_pubkey, memory_keypair.pubkey(), - Some(deposit_reserve.collateral_host), + Some(deposit_reserve.liquidity_host), ), ], Some(&payer.pubkey()), @@ -734,8 +734,8 @@ pub struct TestReserve { pub liquidity_supply: Pubkey, pub collateral_mint: Pubkey, pub collateral_supply: Pubkey, - pub collateral_fees_receiver: Pubkey, - pub collateral_host: Pubkey, + pub liquidity_fees_receiver: Pubkey, + pub liquidity_host: Pubkey, pub user_liquidity_account: Pubkey, pub user_collateral_account: Pubkey, pub dex_market: Option, @@ -759,9 +759,9 @@ impl TestReserve { let reserve_pubkey = reserve_keypair.pubkey(); let collateral_mint_keypair = Keypair::new(); let collateral_supply_keypair = Keypair::new(); - let collateral_fees_receiver_keypair = Keypair::new(); - let collateral_host_keypair = Keypair::new(); let liquidity_supply_keypair = Keypair::new(); + let liquidity_fees_receiver_keypair = Keypair::new(); + let liquidity_host_keypair = Keypair::new(); let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority_keypair = Keypair::new(); @@ -806,21 +806,21 @@ impl TestReserve { ), create_account( &payer.pubkey(), - &collateral_fees_receiver_keypair.pubkey(), + &liquidity_supply_keypair.pubkey(), rent.minimum_balance(Token::LEN), Token::LEN as u64, &spl_token::id(), ), create_account( &payer.pubkey(), - &collateral_host_keypair.pubkey(), + &liquidity_fees_receiver_keypair.pubkey(), rent.minimum_balance(Token::LEN), Token::LEN as u64, &spl_token::id(), ), create_account( &payer.pubkey(), - &liquidity_supply_keypair.pubkey(), + &liquidity_host_keypair.pubkey(), rent.minimum_balance(Token::LEN), Token::LEN as u64, &spl_token::id(), @@ -848,9 +848,9 @@ impl TestReserve { reserve_pubkey, liquidity_mint_pubkey, liquidity_supply_keypair.pubkey(), - collateral_mint_keypair.pubkey(), + liquidity_mint_pubkey, collateral_supply_keypair.pubkey(), - collateral_fees_receiver_keypair.pubkey(), + liquidity_fees_receiver_keypair.pubkey(), lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority_keypair.pubkey(), @@ -869,9 +869,9 @@ impl TestReserve { &lending_market.owner, &collateral_mint_keypair, &collateral_supply_keypair, - &collateral_fees_receiver_keypair, - &collateral_host_keypair, &liquidity_supply_keypair, + &liquidity_fees_receiver_keypair, + &liquidity_host_keypair, &user_collateral_token_keypair, &user_transfer_authority_keypair, ], @@ -889,10 +889,10 @@ impl TestReserve { liquidity_mint: liquidity_mint_pubkey, liquidity_mint_decimals: liquidity_mint.decimals, liquidity_supply: liquidity_supply_keypair.pubkey(), + liquidity_fees_receiver: liquidity_fees_receiver_keypair.pubkey(), + liquidity_host: liquidity_host_keypair.pubkey(), collateral_mint: collateral_mint_keypair.pubkey(), collateral_supply: collateral_supply_keypair.pubkey(), - collateral_fees_receiver: collateral_fees_receiver_keypair.pubkey(), - collateral_host: collateral_host_keypair.pubkey(), user_liquidity_account, user_collateral_account: user_collateral_token_keypair.pubkey(), dex_market: dex_market_pubkey, @@ -918,11 +918,11 @@ impl TestReserve { self.name, self.collateral_supply ); genesis_accounts - .fetch_and_insert(banks_client, self.collateral_fees_receiver) + .fetch_and_insert(banks_client, self.liquidity_fees_receiver) .await; println!( "{}_collateral_fees_receiver: {}", - self.name, self.collateral_fees_receiver + self.name, self.liquidity_fees_receiver ); genesis_accounts .fetch_and_insert(banks_client, self.collateral_supply) diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 074eb30dbb3..e213dbbcc62 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -119,7 +119,7 @@ async fn test_already_initialized() { usdc_reserve.liquidity_supply, usdc_reserve.collateral_mint, usdc_reserve.collateral_supply, - usdc_reserve.collateral_fees_receiver, + usdc_reserve.liquidity_fees_receiver, lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority.pubkey(), From a2d0a784f9ae071fbdc2984d5609733e88248a45 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 12:39:50 -0600 Subject: [PATCH 005/191] fixme comment --- token-lending/client/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/client/src/main.rs b/token-lending/client/src/main.rs index 78fa1ca0bfb..f5e60396b3e 100644 --- a/token-lending/client/src/main.rs +++ b/token-lending/client/src/main.rs @@ -240,6 +240,7 @@ pub fn create_reserve( .get_minimum_balance_for_rent_exemption(Reserve::LEN) .unwrap(), Reserve::LEN as u64, + // @FIXME: shouldn't all references like this be spl_token::id()? &id(), ), ], From 69e15ebc8b4df0d52077a475fa711545678f20d4 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 17:48:42 -0600 Subject: [PATCH 006/191] obligation structs + pack --- token-lending/program/src/state/obligation.rs | 273 ++++++++++++++---- 1 file changed, 218 insertions(+), 55 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 80bd8bb7486..1bb9ff7c4e2 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -5,6 +5,7 @@ use crate::{ }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ + clock::Slot, entrypoint::ProgramResult, program_error::ProgramError, program_pack::{IsInitialized, Pack, Sealed}, @@ -17,38 +18,63 @@ use std::convert::TryInto; pub struct Obligation { /// Version of the obligation pub version: u8, - /// Amount of collateral tokens deposited for this obligation - pub deposited_collateral_tokens: u64, - /// Reserve which collateral tokens were deposited into - pub collateral_reserve: Pubkey, - /// Borrow rate used for calculating interest. - pub cumulative_borrow_rate_wads: Decimal, - /// Amount of tokens borrowed for this obligation plus interest - pub borrowed_liquidity_wads: Decimal, - /// Reserve which tokens were borrowed from - pub borrow_reserve: Pubkey, + /// Collateral accounts for the obligation + pub collateral: Vec, + /// Liquidity accounts for the obligation + pub liquidity: Vec, /// Mint address of the tokens for this obligation pub token_mint: Pubkey, + /// Ratio of liquidity market value to collateral market value (weighted average) + pub loan_to_value: Decimal, + /// Last slot when collateral, liquidity, or loan to value updated + pub last_update_slot: Slot, +} + +/// Obligation collateral state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationCollateral { + /// Reserve which collateral tokens were deposited into + pub deposit_reserve: Pubkey, + /// Amount of collateral tokens deposited for an obligation + pub deposited_tokens: u64, + /// Market value of collateral + pub market_value: u64, + /// Last slot when market value updated + pub last_update_slot: Slot, +} + +/// Obligation liquidity state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationLiquidity { + /// Reserve which liquidity tokens were borrowed from + pub borrow_reserve: Pubkey, + /// Borrow rate used for calculating interest + pub cumulative_borrow_rate_wads: Decimal, + /// Amount of liquidity tokens borrowed for an obligation plus interest + pub borrowed_wads: Decimal, + /// Market value of liquidity + pub market_value: u64, + /// Last slot when market value and accrued interest updated + pub last_update_slot: Slot, } impl Obligation { /// Create new obligation pub fn new(params: NewObligationParams) -> Self { let NewObligationParams { - collateral_reserve, - borrow_reserve, + collateral, + liquidity, token_mint, - cumulative_borrow_rate_wads, + current_slot, } = params; Self { version: PROGRAM_VERSION, - deposited_collateral_tokens: 0, - collateral_reserve, - cumulative_borrow_rate_wads, - borrowed_liquidity_wads: Decimal::zero(), - borrow_reserve, + collateral, + liquidity, token_mint, + loan_to_value: Decimal::zero(), + last_update_slot: current_slot, } } @@ -157,6 +183,13 @@ impl Obligation { obligation_token_amount, }) } + + /// Return slots elapsed since last update + fn update_slot(&mut self, slot: Slot) -> u64 { + let slots_elapsed = slot - self.last_update_slot; + self.last_update_slot = slot; + slots_elapsed + } } /// Obligation repay result @@ -173,14 +206,14 @@ pub struct RepayResult { /// Create new obligation pub struct NewObligationParams { - /// Collateral reserve address - pub collateral_reserve: Pubkey, - /// Borrow reserve address - pub borrow_reserve: Pubkey, + /// Collateral for the obligation + pub collateral: Vec, + /// Liquidity for the obligation + pub liquidity: Vec, /// Obligation token mint address pub token_mint: Pubkey, - /// Borrow rate used for calculating interest. - pub cumulative_borrow_rate_wads: Decimal, + /// Current slot + pub current_slot: Slot, } impl Sealed for Obligation {} @@ -190,58 +223,188 @@ impl IsInitialized for Obligation { } } -const OBLIGATION_LEN: usize = 265; +const MAX_PUBKEYS: usize = 10; +const PUBKEY_LEN: usize = 32; + +const OBLIGATION_LEN: usize = 379; // 1 + 8 + 16 + 32 + 1 + 1 + (32 * 10) impl Pack for Obligation { - const LEN: usize = 265; + const LEN: usize = OBLIGATION_LEN; - /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). - fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, OBLIGATION_LEN]; + fn pack_into_slice(&self, dst: &mut [u8]) { + let dst = array_mut_ref![dst, 0, OBLIGATION_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( version, - deposited_collateral_tokens, - collateral_supply, - cumulative_borrow_rate, - borrowed_liquidity_wads, - borrow_reserve, + last_update_slot, + loan_to_value, token_mint, - _padding, - ) = array_refs![input, 1, 8, 32, 16, 16, 32, 32, 128]; + num_collateral, + num_liquidity, + pubkeys_flat, + ) = mut_array_refs![dst, 1, 8, 16, PUBKEY_LEN, 1, 1, PUBKEY_LEN * MAX_PUBKEYS]; + + *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + pack_decimal(self.loan_to_value, loan_to_value); + token_mint.copy_from_slice(self.token_mint.as_ref()); + + let collateral_len = self.collateral.len(); + let liquidity_len = self.liquidity.len(); + + *num_collateral = collateral_len.to_le_bytes(); + *num_liquidity = liquidity_len.to_le_bytes(); + + let mut offset = 0; + for src in self.collateral.iter() { + let dst_array = array_mut_ref![pubkeys_flat, offset, PUBKEY_LEN]; + dst_array.copy_from_slice(src.as_ref()); + offset += PUBKEY_LEN; + } + for src in self.liquidity.iter() { + let dst_array = array_mut_ref![pubkeys_flat, offset, PUBKEY_LEN]; + dst_array.copy_from_slice(src.as_ref()); + offset += PUBKEY_LEN; + } + } + + fn unpack_from_slice(src: &[u8]) -> Result { + let src = array_ref![src, 0, OBLIGATION_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + last_update_slot, + loan_to_value, + token_mint, + num_collateral, + num_liquidity, + pubkeys_flat, + ) = array_refs![src, 1, 8, 16, PUBKEY_LEN, 1, 1, PUBKEY_LEN * MAX_PUBKEYS]; + + let collateral_len = u8::from_le_bytes(*num_collateral); + let liquidity_len = u8::from_le_bytes(*num_liquidity); + let total_len = collateral_len + liquidity_len; + + let mut collateral = vec![]; + let mut liquidity = vec![]; + + let mut offset = 0; + for src in pubkeys_flat.chunks(PUBKEY_LEN) { + if offset < collateral_len { + collateral.push(Pubkey::new(src)); + } else if offset < total_len { + liquidity.push(Pubkey::new(src)); + } else { + break; + } + offset += 1; + } + Ok(Self { version: u8::from_le_bytes(*version), - deposited_collateral_tokens: u64::from_le_bytes(*deposited_collateral_tokens), - collateral_reserve: Pubkey::new_from_array(*collateral_supply), - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate), - borrowed_liquidity_wads: unpack_decimal(borrowed_liquidity_wads), - borrow_reserve: Pubkey::new_from_array(*borrow_reserve), + last_update_slot: u64::from_le_bytes(*last_update_slot), + loan_to_value: unpack_decimal(loan_to_value), token_mint: Pubkey::new_from_array(*token_mint), + collateral, + liquidity, }) } +} + +impl Sealed for ObligationCollateral {} + +const OBLIGATION_COLLATERAL_LEN: usize = 184; // 32 + 8 + 8 + 8 + 128 +impl Pack for ObligationCollateral { + const LEN: usize = OBLIGATION_COLLATERAL_LEN; + + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, OBLIGATION_COLLATERAL_LEN]; + let (deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = + mut_array_refs![output, PUBKEY_LEN, 8, 8, 8, 128]; + + deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); + *deposited_tokens = self.deposited_tokens.to_le_bytes(); + *market_value = self.market_value.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = + array_refs![input, PUBKEY_LEN, 8, 8, 8, 128]; + + Ok(Self { + deposit_reserve: Pubkey::new_from_array(*deposit_reserve), + deposited_tokens: u64::from_le_bytes(*deposited_tokens), + market_value: u64::from_le_bytes(*market_value), + last_update_slot: u64::from_le_bytes(*last_update_slot), + }) + } +} + +impl Sealed for ObligationLiquidity {} + +// /// Reserve which liquidity tokens were borrowed from +// pub borrow_reserve: Pubkey, +// /// Borrow rate used for calculating interest +// pub cumulative_borrow_rate_wads: Decimal, +// /// Amount of liquidity tokens borrowed for an obligation plus interest +// pub borrowed_wads: Decimal, +// /// Market value of liquidity +// pub market_value: u64, +// /// Last slot when market value and accrued interest updated +// pub last_update_slot: Slot, + +const OBLIGATION_LIQUIDITY_LEN: usize = 208; // 32 + 16 + 16 + 8 + 8 + 128 +impl Pack for ObligationLiquidity { + const LEN: usize = OBLIGATION_LIQUIDITY_LEN; fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, OBLIGATION_LEN]; + let output = array_mut_ref![output, 0, OBLIGATION_LIQUIDITY_LEN]; let ( - version, - deposited_collateral_tokens, - collateral_supply, - cumulative_borrow_rate, - borrowed_liquidity_wads, borrow_reserve, - token_mint, + cumulative_borrow_rate_wads, + borrowed_wads, + market_value, + last_update_slot, _padding, - ) = mut_array_refs![output, 1, 8, 32, 16, 16, 32, 32, 128]; + ) = mut_array_refs![output, PUBKEY_LEN, 16, 16, 8, 8, 128]; - *version = self.version.to_le_bytes(); - *deposited_collateral_tokens = self.deposited_collateral_tokens.to_le_bytes(); - collateral_supply.copy_from_slice(self.collateral_reserve.as_ref()); - pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate); - pack_decimal(self.borrowed_liquidity_wads, borrowed_liquidity_wads); borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); - token_mint.copy_from_slice(self.token_mint.as_ref()); + pack_decimal( + self.cumulative_borrow_rate_wads, + cumulative_borrow_rate_wads, + ); + pack_decimal(self.borrowed_wads, borrowed_wads); + *market_value = self.market_value.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, OBLIGATION_LIQUIDITY_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_wads, + market_value, + last_update_slot, + _padding, + ) = array_refs![input, PUBKEY_LEN, 16, 16, 8, 8, 128]; + + Ok(Self { + borrow_reserve: Pubkey::new_from_array(*borrow_reserve), + cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), + borrowed_wads: unpack_decimal(borrowed_wads), + market_value: u64::from_le_bytes(*market_value), + last_update_slot: u64::from_le_bytes(*last_update_slot), + }) } } +// @FIXME: tests #[cfg(test)] mod test { use super::*; From c142c7dd8984dd40cec75045fb4b3930df66ddcb Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 17:49:12 -0600 Subject: [PATCH 007/191] make cumulative_borrow_rate_wads consistent --- token-lending/program/src/processor.rs | 4 ++-- token-lending/program/src/state/obligation.rs | 14 +++++++------- token-lending/program/src/state/reserve.rs | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c6edf786b09..aa0b3f53f56 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -382,11 +382,11 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro assert_uninitialized::(obligation_info)?; assert_last_update_slot(&borrow_reserve, clock.slot)?; - let cumulative_borrow_rate = borrow_reserve.cumulative_borrow_rate_wads; + let cumulative_borrow_rate_wads = borrow_reserve.cumulative_borrow_rate_wads; let obligation_mint_decimals = deposit_reserve.liquidity.mint_decimals; let obligation = Obligation::new(NewObligationParams { collateral_reserve: *deposit_reserve_info.key, - cumulative_borrow_rate_wads: cumulative_borrow_rate, + cumulative_borrow_rate_wads: cumulative_borrow_rate_wads, borrow_reserve: *borrow_reserve_info.key, token_mint: *obligation_token_mint_info.key, }); diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 1bb9ff7c4e2..a130a627047 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -122,12 +122,12 @@ impl Obligation { } /// Accrue interest - pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult { - if cumulative_borrow_rate < self.cumulative_borrow_rate_wads { + pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { + if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { return Err(LendingError::NegativeInterestRate.into()); } - let compounded_interest_rate: Rate = cumulative_borrow_rate + let compounded_interest_rate: Rate = cumulative_borrow_rate_wads .try_div(self.cumulative_borrow_rate_wads)? .try_into()?; @@ -135,7 +135,7 @@ impl Obligation { .borrowed_liquidity_wads .try_mul(compounded_interest_rate)?; - self.cumulative_borrow_rate_wads = cumulative_borrow_rate; + self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; Ok(()) } @@ -546,10 +546,10 @@ mod test { ..Obligation::default() }; - let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; - state.accrue_interest(next_cumulative_borrow_rate)?; + let next_cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; + state.accrue_interest(next_cumulative_borrow_rate_wads)?; - if next_cumulative_borrow_rate > cumulative_borrow_rate_wads { + if next_cumulative_borrow_rate_wads > cumulative_borrow_rate_wads { assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); } else { assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 200725e1d4b..2ed221a568b 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -635,7 +635,7 @@ impl Pack for Reserve { max_borrow_rate, borrow_fee_wad, host_fee_percentage, - cumulative_borrow_rate, + cumulative_borrow_rate_wads, total_borrows, available_liquidity, collateral_mint_supply, @@ -647,7 +647,7 @@ impl Pack for Reserve { Ok(Self { version: u8::from_le_bytes(*version), last_update_slot: u64::from_le_bytes(*last_update_slot), - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate), + cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), lending_market: Pubkey::new_from_array(*lending_market), dex_market: unpack_coption_key(dex_market)?, liquidity: ReserveLiquidity { @@ -701,7 +701,7 @@ impl Pack for Reserve { max_borrow_rate, borrow_fee_wad, host_fee_percentage, - cumulative_borrow_rate, + cumulative_borrow_rate_wads, total_borrows, available_liquidity, collateral_mint_supply, @@ -712,7 +712,7 @@ impl Pack for Reserve { ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); - pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate); + pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate_wads); lending_market.copy_from_slice(self.lending_market.as_ref()); pack_coption_key(&self.dex_market, dex_market); From 772fbc4ea69b7517ba7d46945e24c9a15f6ef9bb Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 11 Mar 2021 22:15:57 -0600 Subject: [PATCH 008/191] more instructions --- token-lending/program/src/instruction.rs | 276 ++++++++++++++++++----- 1 file changed, 224 insertions(+), 52 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 821348804d1..47f1ecd4c01 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -214,7 +214,7 @@ pub enum LendingInstruction { AccrueReserveInterest, // 9 - /// Deposit additional collateral to an obligation. Requires a recently refreshed obligation. + /// Deposit collateral to an obligation. Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// @@ -273,50 +273,79 @@ pub enum LendingInstruction { }, // 12 + /// Initializes a new loan obligation collateral. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation + /// 1. `[writable]` Obligation collateral + /// 2. `[]` Deposit reserve account. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[]` Clock sysvar + /// 7. `[]` Rent sysvar + /// 8. `[]` Token program id + InitObligationCollateral, + + // 13 + /// Initializes a new loan obligation liquidity. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation + /// 1. `[writable]` Obligation collateral + /// 2. `[]` Borrow reserve account. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[]` Clock sysvar + /// 7. `[]` Rent sysvar + /// 8. `[]` Token program id + InitObligationLiquidity, + + // 14 /// Refresh market value of an obligation's collateral. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation collateral account /// 1. `[]` Deposit reserve account - /// 2. `[]` Dex market - /// 3. `[]` Dex market order book side - /// 4. `[]` Lending market account - /// 5. `[]` Derived lending market authority + /// 2. `[]` Lending market account + /// 3. `[]` Derived lending market authority + /// 4. `[]` Dex market + /// 5. `[]` Dex market order book side /// 6. `[]` Temporary memory /// 7. `[]` Clock sysvar /// 8. `[]` Token program id RefreshObligationCollateral, - // 13 + // 15 /// Refresh market value and accrued interest of an obligation's liquidity. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation liquidity account /// 1. `[]` Borrow reserve account - /// 2. `[]` Dex market - /// 3. `[]` Dex market order book side - /// 4. `[]` Lending market account - /// 5. `[]` Derived lending market authority + /// 2. `[]` Lending market account + /// 3. `[]` Derived lending market authority + /// 4. `[]` Dex market + /// 5. `[]` Dex market order book side /// 6. `[]` Temporary memory /// 7. `[]` Clock sysvar /// 8. `[]` Token program id RefreshObligationLiquidity, - // 14 + // 16 /// Refresh an obligation's loan to value ratio. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation account - /// 1..1+NUM_COL `[]` Obligation collateral accounts - /// 2+NUM_COL..2+NUM_LIQ `[]` Obligation liquidity accounts - /// 3+NUM_COL+NUM_LIQ `[]` Lending market account - /// 4+NUM_COL+NUM_LIQ `[]` Derived lending market authority - /// 5+NUM_COL+NUM_LIQ `[]` Temporary memory - /// 6+NUM_COL+NUM_LIQ `[]` Clock sysvar - /// 7+NUM_COL+NUM_LIQ `[]` Token program id + /// 1..1+N `[]` All obligation collateral accounts, followed by obligation liquidity accounts + /// 2+N `[]` Lending market account + /// 3+N `[]` Derived lending market authority + /// 4+N `[]` Temporary memory + /// 5+N `[]` Clock sysvar + /// 6+N `[]` Token program id RefreshObligation, } @@ -399,9 +428,11 @@ impl LendingInstruction { let (new_owner, _rest) = Self::unpack_pubkey(rest)?; Self::SetLendingMarketOwner { new_owner } } - 12 => Self::RefreshObligationCollateral, - 13 => Self::RefreshObligationLiquidity, - 14 => Self::RefreshObligation, + 12 => Self::InitObligationCollateral, + 13 => Self::InitObligationLiquidity, + 14 => Self::RefreshObligationCollateral, + 15 => Self::RefreshObligationLiquidity, + 16 => Self::RefreshObligation, _ => return Err(LendingError::InstructionUnpackError.into()), }) } @@ -524,15 +555,21 @@ impl LendingInstruction { buf.push(11); buf.extend_from_slice(new_owner.as_ref()); } - Self::RefreshObligationCollateral => { + Self::InitObligationCollateral => { buf.push(12); } - Self::RefreshObligationLiquidity => { + Self::InitObligationLiquidity => { buf.push(13); } - Self::RefreshObligation => { + Self::RefreshObligationCollateral => { buf.push(14); } + Self::RefreshObligationLiquidity => { + buf.push(15); + } + Self::RefreshObligation => { + buf.push(16); + } } buf } @@ -598,11 +635,9 @@ pub fn init_reserve( AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; - if let Some(dex_market_pubkey) = dex_market_pubkey { accounts.push(AccountMeta::new_readonly(dex_market_pubkey, false)); } - Instruction { program_id, accounts, @@ -618,32 +653,27 @@ pub fn init_reserve( #[allow(clippy::too_many_arguments)] pub fn init_obligation( program_id: Pubkey, - deposit_reserve_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - lending_market_pubkey: Pubkey, obligation_pubkey: Pubkey, obligation_token_mint_pubkey: Pubkey, obligation_token_output_pubkey: Pubkey, obligation_token_owner_pubkey: Pubkey, + lending_market_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); - let accounts = vec![ - AccountMeta::new_readonly(deposit_reserve_pubkey, false), - AccountMeta::new_readonly(borrow_reserve_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), - AccountMeta::new_readonly(obligation_token_owner_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ]; Instruction { program_id, - accounts, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_token_mint_pubkey, false), + AccountMeta::new(obligation_token_output_pubkey, false), + AccountMeta::new_readonly(obligation_token_owner_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], data: LendingInstruction::InitObligation.pack(), } } @@ -857,7 +887,8 @@ pub fn liquidate_obligation( /// Creates an `AccrueReserveInterest` instruction pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { - let mut accounts = vec![AccountMeta::new_readonly(sysvar::clock::id(), false)]; + let mut accounts = Vec::with_capacity(1 + reserve_pubkeys.len()); + accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)); accounts.extend( reserve_pubkeys .into_iter() @@ -879,6 +910,7 @@ pub fn deposit_obligation_collateral( destination_collateral_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, obligation_mint_pubkey: Pubkey, obligation_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, @@ -892,11 +924,13 @@ pub fn deposit_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new(obligation_mint_pubkey, false), AccountMeta::new(obligation_output_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), @@ -911,16 +945,13 @@ pub fn withdraw_obligation_collateral( source_collateral_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, withdraw_reserve_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, obligation_mint_pubkey: Pubkey, obligation_input_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, ) -> Instruction { Instruction { program_id, @@ -928,16 +959,13 @@ pub fn withdraw_obligation_collateral( AccountMeta::new(source_collateral_pubkey, false), AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(withdraw_reserve_pubkey, false), - AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new(obligation_mint_pubkey, false), AccountMeta::new(obligation_input_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], @@ -961,3 +989,147 @@ pub fn set_lending_market_owner( data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), } } + +/// Creates an 'InitObligationCollateral' instruction. +#[allow(clippy::too_many_arguments)] +pub fn init_obligation_collateral( + program_id: Pubkey, + obligation_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, + deposit_reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), + AccountMeta::new_readonly(deposit_reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::InitObligationCollateral.pack(), + } +} + +/// Creates an 'InitObligationLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn init_obligation_liquidity( + program_id: Pubkey, + obligation_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + borrow_reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new_readonly(borrow_reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::InitObligationLiquidity.pack(), + } +} + +/// Creates a 'RefreshObligationCollateral' instruction. +#[allow(clippy::too_many_arguments)] +pub fn refresh_obligation_collateral( + program_id: Pubkey, + obligation_collateral_pubkey: Pubkey, + deposit_reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + dex_market_pubkey: Pubkey, + dex_market_order_book_side_pubkey: Pubkey, + memory_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_collateral_pubkey, false), + AccountMeta::new_readonly(deposit_reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(dex_market_pubkey, false), + AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), + AccountMeta::new_readonly(memory_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::RefreshObligationCollateral.pack(), + } +} + +/// Creates a 'RefreshObligationLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn refresh_obligation_liquidity( + program_id: Pubkey, + obligation_liquidity_pubkey: Pubkey, + borrow_reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + dex_market_pubkey: Pubkey, + dex_market_order_book_side_pubkey: Pubkey, + memory_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new_readonly(borrow_reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(dex_market_pubkey, false), + AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), + AccountMeta::new_readonly(memory_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::RefreshObligationLiquidity.pack(), + } +} + +/// Creates a 'RefreshObligation' instruction. +#[allow(clippy::too_many_arguments)] +pub fn refresh_obligation( + program_id: Pubkey, + obligation_pubkey: Pubkey, + obligation_collateral_liquidity_pubkeys: Vec, + lending_market_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + let mut accounts = Vec::with_capacity(5 + obligation_collateral_liquidity_pubkeys.len()); + accounts.push(AccountMeta::new(obligation_pubkey, false)); + accounts.extend( + obligation_collateral_liquidity_pubkeys + .into_iter() + .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + ); + accounts.extend(vec![ + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ]); + Instruction { + program_id, + accounts, + data: LendingInstruction::RefreshObligationLiquidity.pack(), + } +} From 6430410c12f321bdf303cce1924c325250c61db6 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:34:05 -0600 Subject: [PATCH 009/191] add errors --- token-lending/program/src/error.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index ccf483b0a80..4503faf78c6 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -126,7 +126,20 @@ pub enum LendingError { #[error("Token burn failed")] TokenBurnFailed, + // @FIXME: split up after 35 // 35 + // @FIXME: change name + message + /// ObligationAccountLimit + #[error("ObligationAccountLimit")] + ObligationAccountLimit, + // @FIXME: change name + message + /// ObligationAccountDuplicate + #[error("ObligationAccountDuplicate")] + ObligationAccountDuplicate, + // @FIXME: change name + message + /// ObligationAccountNotFound + #[error("ObligationAccountNotFound")] + ObligationAccountNotFound, /// Invalid obligation collateral amount #[error("Invalid obligation collateral amount")] InvalidObligationCollateral, From 4176afe0997155fbd49705821450ed898b5521c0 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:34:14 -0600 Subject: [PATCH 010/191] remove unused import --- token-lending/program/src/processor.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index aa0b3f53f56..ffb9a54555c 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -24,7 +24,6 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; -use spl_token::state::Account as Token; /// Processes an instruction pub fn process_instruction( From 07219d7135a44ded763ce9a4762094a717ecf612 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:34:33 -0600 Subject: [PATCH 011/191] add new instructions --- token-lending/program/src/processor.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ffb9a54555c..0e80f1164fc 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -87,6 +87,26 @@ pub fn process_instruction( msg!("Instruction: Set Lending Market Owner"); process_set_lending_market_owner(program_id, new_owner, accounts) } + LendingInstruction::InitObligationCollateral => { + msg!("Instruction: Init Obligation Collateral"); + process_init_obligation_collateral(program_id, accounts) + } + LendingInstruction::InitObligationLiquidity => { + msg!("Instruction: Init Obligation Liquidity"); + process_init_obligation_liquidity(program_id, accounts) + } + LendingInstruction::RefreshObligationCollateral => { + msg!("Instruction: Refresh Obligation Collateral"); + process_refresh_obligation_collateral(program_id, accounts) + } + LendingInstruction::RefreshObligationLiquidity => { + msg!("Instruction: Refresh Obligation Liquidity"); + process_refresh_obligation_liquidity(program_id, accounts) + } + LendingInstruction::RefreshObligation => { + msg!("Instruction: Refresh Obligation"); + process_refresh_obligation(program_id, accounts) + } } } From aa0a745136a721f1311b3afafddd87935ca33abd Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:44:57 -0600 Subject: [PATCH 012/191] check lending market authority before packing --- token-lending/program/src/processor.rs | 102 +++++++++++++------------ 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 0e80f1164fc..a42a8b926fa 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -496,10 +496,6 @@ fn process_deposit_reserve_liquidity( return Err(LendingError::InvalidAccountInput.into()); } - assert_last_update_slot(&reserve, clock.slot)?; - let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -510,6 +506,11 @@ fn process_deposit_reserve_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } + assert_last_update_slot(&reserve, clock.slot)?; + + let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: reserve_liquidity_supply_info.clone(), @@ -585,10 +586,6 @@ fn process_withdraw_reserve_liquidity( return Err(LendingError::InvalidAccountInput.into()); } - assert_last_update_slot(&reserve, clock.slot)?; - let liquidity_withdraw_amount = reserve.redeem_collateral(collateral_amount)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -599,6 +596,11 @@ fn process_withdraw_reserve_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } + assert_last_update_slot(&reserve, clock.slot)?; + + let liquidity_withdraw_amount = reserve.redeem_collateral(collateral_amount)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + spl_token_burn(TokenBurnParams { mint: reserve_collateral_mint_info.clone(), source: source_collateral_info.clone(), @@ -758,8 +760,19 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidTokenMint.into()); } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + assert_last_update_slot(&borrow_reserve, clock.slot)?; assert_last_update_slot(&deposit_reserve, clock.slot)?; + obligation.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; let trade_simulator = TradeSimulator::new( @@ -787,16 +800,6 @@ fn process_borrow_obligation_liquidity( Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - // deposit collateral spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), @@ -954,8 +957,19 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + // accrue interest and update rates assert_last_update_slot(&repay_reserve, clock.slot)?; + obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; let RepayResult { @@ -971,16 +985,6 @@ fn process_repay_obligation_liquidity( Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - // burn obligation tokens spl_token_burn(TokenBurnParams { mint: obligation_token_mint_info.clone(), @@ -1128,9 +1132,20 @@ fn process_liquidate_obligation( } } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + // accrue interest and update rates assert_last_update_slot(&repay_reserve, clock.slot)?; assert_last_update_slot(&withdraw_reserve, clock.slot)?; + obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; let trade_simulator = TradeSimulator::new( @@ -1159,16 +1174,6 @@ fn process_liquidate_obligation( obligation.liquidate(settle_amount, withdraw_amount)?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - // deposit repaid liquidity spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), @@ -1441,6 +1446,16 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidTokenMint.into()); } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + // accrue interest and update rates assert_last_update_slot(&borrow_reserve, clock.slot)?; assert_last_update_slot(&withdraw_reserve, clock.slot)?; @@ -1484,19 +1499,8 @@ fn process_withdraw_obligation_collateral( .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; obligation.deposited_collateral_tokens = remaining_collateral; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - // burn obligation tokens spl_token_burn(TokenBurnParams { mint: obligation_token_mint_info.clone(), From dedc603b9e16fd5a02fb2e6e75d04bd2b3a74253 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:47:39 -0600 Subject: [PATCH 013/191] format --- token-lending/program/src/processor.rs | 4 +--- token-lending/program/src/state/reserve.rs | 20 ++++++++++++-------- token-lending/program/tests/helpers/mod.rs | 6 ++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index a42a8b926fa..f7825c4c900 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -708,9 +708,7 @@ fn process_borrow_obligation_liquidity( msg!("Cannot use borrow reserve liquidity supply as destination account input"); return Err(LendingError::InvalidAccountInput.into()); } - if &borrow_reserve.liquidity.fees_receiver - != borrow_reserve_liquidity_fees_receiver_info.key - { + if &borrow_reserve.liquidity.fees_receiver != borrow_reserve_liquidity_fees_receiver_info.key { msg!("Invalid borrow reserve liquidity fees receiver account"); return Err(LendingError::InvalidAccountInput.into()); } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 2ed221a568b..4d2c9dec0f1 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -212,8 +212,7 @@ impl Reserve { } }; - let (origination_fee, host_fee) = - self.config.fees.calculate_borrow_fees(borrow_amount)?; + let (origination_fee, host_fee) = self.config.fees.calculate_borrow_fees(borrow_amount)?; // @FIXME: fees collateral_amount = collateral_amount @@ -396,7 +395,12 @@ pub struct ReserveLiquidity { impl ReserveLiquidity { /// New reserve liquidity info - pub fn new(mint_pubkey: Pubkey, mint_decimals: u8, supply_pubkey: Pubkey, fees_receiver: Pubkey) -> Self { + pub fn new( + mint_pubkey: Pubkey, + mint_decimals: u8, + supply_pubkey: Pubkey, + fees_receiver: Pubkey, + ) -> Self { Self { mint_pubkey, mint_decimals, @@ -564,10 +568,7 @@ pub struct ReserveFees { impl ReserveFees { /// Calculate the owner and host fees on borrow - pub fn calculate_borrow_fees( - &self, - borrow_amount: u64, - ) -> Result<(u64, u64), ProgramError> { + pub fn calculate_borrow_fees(&self, borrow_amount: u64) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); if borrow_fee_rate > Rate::zero() && borrow_amount > 0 { @@ -712,7 +713,10 @@ impl Pack for Reserve { ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); - pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate_wads); + pack_decimal( + self.cumulative_borrow_rate_wads, + cumulative_borrow_rate_wads, + ); lending_market.copy_from_slice(self.lending_market.as_ref()); pack_coption_key(&self.dex_market, dex_market); diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 2256efce43f..9862f4122d7 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -345,10 +345,8 @@ pub fn add_reserve( liquidity_supply_pubkey, liquidity_fees_receiver_pubkey, ); - let reserve_collateral = ReserveCollateral::new( - collateral_mint_pubkey, - collateral_supply_pubkey, - ); + let reserve_collateral = + ReserveCollateral::new(collateral_mint_pubkey, collateral_supply_pubkey); let mut reserve = Reserve::new(NewReserveParams { // intentionally wrapped to simulate elapsed slots current_slot: 1u64.wrapping_sub(slots_elapsed), From 5d02229599ff6afdab0c321fbf8ec713cb6e6662 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:50:36 -0600 Subject: [PATCH 014/191] split up collateral state --- token-lending/program/src/state/mod.rs | 7 + token-lending/program/src/state/obligation.rs | 185 ++---------------- .../src/state/obligation_collateral.rs | 104 ++++++++++ .../program/src/state/obligation_liquidity.rs | 161 +++++++++++++++ 4 files changed, 285 insertions(+), 172 deletions(-) create mode 100644 token-lending/program/src/state/obligation_collateral.rs create mode 100644 token-lending/program/src/state/obligation_liquidity.rs diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 832b8d712af..1434fafdb3d 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -2,10 +2,14 @@ mod lending_market; mod obligation; +mod obligation_collateral; +mod obligation_liquidity; mod reserve; pub use lending_market::*; pub use obligation::*; +pub use obligation_collateral::*; +pub use obligation_liquidity::*; pub use reserve::*; use crate::math::{Decimal, WAD}; @@ -32,6 +36,9 @@ pub const UNINITIALIZED_VERSION: u8 = 0; pub const SLOTS_PER_YEAR: u64 = DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365; +/// Pubkey byte length +pub const PUBKEY_LEN: usize = 32; + /// Token converter pub trait TokenConverter { /// Return best price for specified token diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index a130a627047..ef1deacf587 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -13,6 +13,9 @@ use solana_program::{ }; use std::convert::TryInto; +// @TODO: true max is potentially ~16 +pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; + /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { @@ -30,32 +33,16 @@ pub struct Obligation { pub last_update_slot: Slot, } -/// Obligation collateral state -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ObligationCollateral { - /// Reserve which collateral tokens were deposited into - pub deposit_reserve: Pubkey, - /// Amount of collateral tokens deposited for an obligation - pub deposited_tokens: u64, - /// Market value of collateral - pub market_value: u64, - /// Last slot when market value updated - pub last_update_slot: Slot, -} - -/// Obligation liquidity state -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ObligationLiquidity { - /// Reserve which liquidity tokens were borrowed from - pub borrow_reserve: Pubkey, - /// Borrow rate used for calculating interest - pub cumulative_borrow_rate_wads: Decimal, - /// Amount of liquidity tokens borrowed for an obligation plus interest - pub borrowed_wads: Decimal, - /// Market value of liquidity - pub market_value: u64, - /// Last slot when market value and accrued interest updated - pub last_update_slot: Slot, +/// Create new obligation +pub struct NewObligationParams { + /// Collateral for the obligation + pub collateral: Vec, + /// Liquidity for the obligation + pub liquidity: Vec, + /// Obligation token mint address + pub token_mint: Pubkey, + /// Current slot + pub current_slot: Slot, } impl Obligation { @@ -78,24 +65,6 @@ impl Obligation { } } - /// Maximum amount of loan that can be closed out by a liquidator due - /// to the remaining balance being too small to be liquidated normally. - pub fn max_closeable_amount(&self) -> Result { - if self.borrowed_liquidity_wads < Decimal::from(CLOSEABLE_AMOUNT) { - self.borrowed_liquidity_wads.try_ceil_u64() - } else { - Ok(0) - } - } - - /// Maximum amount of loan that can be repaid by liquidators - pub fn max_liquidation_amount(&self) -> Result { - Ok(self - .borrowed_liquidity_wads - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .try_floor_u64()?) - } - /// Ratio of loan balance to collateral value pub fn loan_to_value( &self, @@ -121,25 +90,6 @@ impl Obligation { token_amount.try_floor_u64() } - /// Accrue interest - pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { - if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { - return Err(LendingError::NegativeInterestRate.into()); - } - - let compounded_interest_rate: Rate = cumulative_borrow_rate_wads - .try_div(self.cumulative_borrow_rate_wads)? - .try_into()?; - - self.borrowed_liquidity_wads = self - .borrowed_liquidity_wads - .try_mul(compounded_interest_rate)?; - - self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; - - Ok(()) - } - /// Liquidate part of obligation pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?; @@ -204,18 +154,6 @@ pub struct RepayResult { pub integer_repay_amount: u64, } -/// Create new obligation -pub struct NewObligationParams { - /// Collateral for the obligation - pub collateral: Vec, - /// Liquidity for the obligation - pub liquidity: Vec, - /// Obligation token mint address - pub token_mint: Pubkey, - /// Current slot - pub current_slot: Slot, -} - impl Sealed for Obligation {} impl IsInitialized for Obligation { fn is_initialized(&self) -> bool { @@ -223,9 +161,6 @@ impl IsInitialized for Obligation { } } -const MAX_PUBKEYS: usize = 10; -const PUBKEY_LEN: usize = 32; - const OBLIGATION_LEN: usize = 379; // 1 + 8 + 16 + 32 + 1 + 1 + (32 * 10) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -310,100 +245,6 @@ impl Pack for Obligation { } } -impl Sealed for ObligationCollateral {} - -const OBLIGATION_COLLATERAL_LEN: usize = 184; // 32 + 8 + 8 + 8 + 128 -impl Pack for ObligationCollateral { - const LEN: usize = OBLIGATION_COLLATERAL_LEN; - - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, OBLIGATION_COLLATERAL_LEN]; - let (deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = - mut_array_refs![output, PUBKEY_LEN, 8, 8, 8, 128]; - - deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); - *deposited_tokens = self.deposited_tokens.to_le_bytes(); - *market_value = self.market_value.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); - } - - /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). - fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, OBLIGATION_COLLATERAL_LEN]; - #[allow(clippy::ptr_offset_with_cast)] - let (deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = - array_refs![input, PUBKEY_LEN, 8, 8, 8, 128]; - - Ok(Self { - deposit_reserve: Pubkey::new_from_array(*deposit_reserve), - deposited_tokens: u64::from_le_bytes(*deposited_tokens), - market_value: u64::from_le_bytes(*market_value), - last_update_slot: u64::from_le_bytes(*last_update_slot), - }) - } -} - -impl Sealed for ObligationLiquidity {} - -// /// Reserve which liquidity tokens were borrowed from -// pub borrow_reserve: Pubkey, -// /// Borrow rate used for calculating interest -// pub cumulative_borrow_rate_wads: Decimal, -// /// Amount of liquidity tokens borrowed for an obligation plus interest -// pub borrowed_wads: Decimal, -// /// Market value of liquidity -// pub market_value: u64, -// /// Last slot when market value and accrued interest updated -// pub last_update_slot: Slot, - -const OBLIGATION_LIQUIDITY_LEN: usize = 208; // 32 + 16 + 16 + 8 + 8 + 128 -impl Pack for ObligationLiquidity { - const LEN: usize = OBLIGATION_LIQUIDITY_LEN; - - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, OBLIGATION_LIQUIDITY_LEN]; - let ( - borrow_reserve, - cumulative_borrow_rate_wads, - borrowed_wads, - market_value, - last_update_slot, - _padding, - ) = mut_array_refs![output, PUBKEY_LEN, 16, 16, 8, 8, 128]; - - borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); - pack_decimal( - self.cumulative_borrow_rate_wads, - cumulative_borrow_rate_wads, - ); - pack_decimal(self.borrowed_wads, borrowed_wads); - *market_value = self.market_value.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); - } - - /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). - fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, OBLIGATION_LIQUIDITY_LEN]; - #[allow(clippy::ptr_offset_with_cast)] - let ( - borrow_reserve, - cumulative_borrow_rate_wads, - borrowed_wads, - market_value, - last_update_slot, - _padding, - ) = array_refs![input, PUBKEY_LEN, 16, 16, 8, 8, 128]; - - Ok(Self { - borrow_reserve: Pubkey::new_from_array(*borrow_reserve), - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), - borrowed_wads: unpack_decimal(borrowed_wads), - market_value: u64::from_le_bytes(*market_value), - last_update_slot: u64::from_le_bytes(*last_update_slot), - }) - } -} - // @FIXME: tests #[cfg(test)] mod test { diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs new file mode 100644 index 00000000000..67b41fee504 --- /dev/null +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -0,0 +1,104 @@ +use std::convert::TryInto; + +use super::*; +use crate::{ + error::LendingError, + math::{Decimal, Rate, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::{ + clock::Slot, + entrypoint::ProgramResult, + program_error::ProgramError, + program_pack::{IsInitialized, Pack, Sealed}, + pubkey::Pubkey, +}; + +/// Obligation collateral state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationCollateral { + /// Version of the obligation collateral + pub version: u8, + /// Reserve which collateral tokens were deposited into + pub deposit_reserve: Pubkey, + /// Amount of collateral tokens deposited for an obligation + pub deposited_tokens: u64, + /// Market value of collateral + pub market_value: u64, + /// Last slot when market value updated + pub last_update_slot: Slot, +} + +/// Create new obligation collateral +pub struct NewObligationCollateralParams { + /// Deposit reserve address + pub deposit_reserve: Pubkey, + /// Current slot + pub current_slot: Slot, +} + +impl ObligationCollateral { + /// Create new obligation collateral + pub fn new(params: NewObligationCollateralParams) -> Self { + let NewObligationCollateralParams { + deposit_reserve, + current_slot, + } = params; + + Self { + version: PROGRAM_VERSION, + deposit_reserve, + deposited_tokens: 0, + market_value: 0, + last_update_slot: current_slot, + } + } + + /// Return slots elapsed since last update + fn update_slot(&mut self, slot: Slot) -> u64 { + // @TODO: checked math? + let slots_elapsed = slot - self.last_update_slot; + self.last_update_slot = slot; + slots_elapsed + } +} + +impl Sealed for ObligationCollateral {} +impl IsInitialized for ObligationCollateral { + fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +const OBLIGATION_COLLATERAL_LEN: usize = 185; // 1 + 32 + 8 + 8 + 8 + 128 +impl Pack for ObligationCollateral { + const LEN: usize = OBLIGATION_COLLATERAL_LEN; + + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, OBLIGATION_COLLATERAL_LEN]; + let (version, deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = + mut_array_refs![output, 1, PUBKEY_LEN, 8, 8, 8, 128]; + + *version = self.version.to_le_bytes(); + deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); + *deposited_tokens = self.deposited_tokens.to_le_bytes(); + *market_value = self.market_value.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (version, deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = + array_refs![input, 1, PUBKEY_LEN, 8, 8, 8, 128]; + + Ok(Self { + version: u8::from_le_bytes(*version), + deposit_reserve: Pubkey::new_from_array(*deposit_reserve), + deposited_tokens: u64::from_le_bytes(*deposited_tokens), + market_value: u64::from_le_bytes(*market_value), + last_update_slot: u64::from_le_bytes(*last_update_slot), + }) + } +} diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs new file mode 100644 index 00000000000..9807d3ee679 --- /dev/null +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -0,0 +1,161 @@ +use std::convert::TryInto; + +use super::*; +use crate::{ + error::LendingError, + math::{Decimal, Rate, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::{ + clock::Slot, + entrypoint::ProgramResult, + program_error::ProgramError, + program_pack::{IsInitialized, Pack, Sealed}, + pubkey::Pubkey, +}; + +/// Obligation liquidity state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationLiquidity { + /// Version of the obligation liquidity + pub version: u8, + /// Reserve which liquidity tokens were borrowed from + pub borrow_reserve: Pubkey, + /// Borrow rate used for calculating interest + pub cumulative_borrow_rate_wads: Decimal, + /// Amount of liquidity tokens borrowed for an obligation plus interest + pub borrowed_wads: Decimal, + /// Market value of liquidity + pub market_value: u64, + /// Last slot when market value and accrued interest updated + pub last_update_slot: Slot, +} + +/// Create new obligation liquidity +pub struct NewObligationLiquidityParams { + /// Borrow reserve address + pub borrow_reserve: Pubkey, + /// Current slot + pub current_slot: Slot, +} + +impl ObligationLiquidity { + /// Create new obligation liquidity + pub fn new(params: NewObligationLiquidityParams) -> Self { + let NewObligationLiquidityParams { + borrow_reserve, + current_slot, + } = params; + + Self { + version: PROGRAM_VERSION, + borrow_reserve, + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_wads: Decimal::zero(), + market_value: 0, + last_update_slot: current_slot, + } + } + + /// Maximum amount of loan that can be closed out by a liquidator due to the remaining balance + /// being too small to be liquidated normally. + pub fn max_closeable_amount(&self) -> Result { + if self.borrowed_wads < Decimal::from(CLOSEABLE_AMOUNT) { + self.borrowed_wads.try_ceil_u64() + } else { + Ok(0) + } + } + + /// Maximum amount of loan that can be repaid by liquidators + pub fn max_liquidation_amount(&self) -> Result { + Ok(self + .borrowed_wads + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? + .try_floor_u64()?) + } + + /// Accrue interest + pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { + if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { + return Err(LendingError::NegativeInterestRate.into()); + } + + let compounded_interest_rate: Rate = cumulative_borrow_rate_wads + .try_div(self.cumulative_borrow_rate_wads)? + .try_into()?; + + self.borrowed_wads = self.borrowed_wads.try_mul(compounded_interest_rate)?; + + self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; + + Ok(()) + } + + /// Return slots elapsed since last update + fn update_slot(&mut self, slot: Slot) -> u64 { + // @TODO: checked math? + let slots_elapsed = slot - self.last_update_slot; + self.last_update_slot = slot; + slots_elapsed + } +} + +impl Sealed for ObligationLiquidity {} +impl IsInitialized for ObligationLiquidity { + fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +const OBLIGATION_LIQUIDITY_LEN: usize = 209; // 1 + 32 + 16 + 16 + 8 + 8 + 128 +impl Pack for ObligationLiquidity { + const LEN: usize = OBLIGATION_LIQUIDITY_LEN; + + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, OBLIGATION_LIQUIDITY_LEN]; + let ( + version, + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_wads, + market_value, + last_update_slot, + _padding, + ) = mut_array_refs![output, 1, PUBKEY_LEN, 16, 16, 8, 8, 128]; + + *version = self.version.to_le_bytes(); + borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); + pack_decimal( + self.cumulative_borrow_rate_wads, + cumulative_borrow_rate_wads, + ); + pack_decimal(self.borrowed_wads, borrowed_wads); + *market_value = self.market_value.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, OBLIGATION_LIQUIDITY_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_wads, + market_value, + last_update_slot, + _padding, + ) = array_refs![input, 1, PUBKEY_LEN, 16, 16, 8, 8, 128]; + + Ok(Self { + version: u8::from_le_bytes(*version), + borrow_reserve: Pubkey::new_from_array(*borrow_reserve), + cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), + borrowed_wads: unpack_decimal(borrowed_wads), + market_value: u64::from_le_bytes(*market_value), + last_update_slot: u64::from_le_bytes(*last_update_slot), + }) + } +} From c6f7b57c888b9d42822efb87647303ecc2015055 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:51:38 -0600 Subject: [PATCH 015/191] use consts where possible --- token-lending/program/src/state/lending_market.rs | 2 +- token-lending/program/src/state/reserve.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 4f80b5da2c1..05d91b69ace 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -30,7 +30,7 @@ impl IsInitialized for LendingMarket { const LENDING_MARKET_LEN: usize = 160; impl Pack for LendingMarket { - const LEN: usize = 160; + const LEN: usize = LENDING_MARKET_LEN; /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html). fn unpack_from_slice(input: &[u8]) -> Result { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 4d2c9dec0f1..dffa99dff25 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -610,7 +610,7 @@ impl IsInitialized for Reserve { const RESERVE_LEN: usize = 602; impl Pack for Reserve { - const LEN: usize = 602; + const LEN: usize = RESERVE_LEN; /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). fn unpack_from_slice(input: &[u8]) -> Result { From 1a8b267fe6e0a26856c5d0b52749c5b61564521c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:52:41 -0600 Subject: [PATCH 016/191] fixme / todo comments --- token-lending/program/src/processor.rs | 7 +++++++ token-lending/program/src/state/obligation.rs | 1 + token-lending/program/src/state/reserve.rs | 1 + 3 files changed, 9 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f7825c4c900..4429fb89665 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -622,6 +622,7 @@ fn process_withdraw_reserve_liquidity( Ok(()) } +// @FIXME #[inline(never)] // avoid stack frame limit fn process_borrow_obligation_liquidity( program_id: &Pubkey, @@ -859,6 +860,7 @@ fn process_borrow_obligation_liquidity( Ok(()) } +// @FIXME #[inline(never)] // avoid stack frame limit fn process_repay_obligation_liquidity( program_id: &Pubkey, @@ -1016,6 +1018,7 @@ fn process_repay_obligation_liquidity( Ok(()) } +// @FIXME #[inline(never)] // avoid stack frame limit fn process_liquidate_obligation( program_id: &Pubkey, @@ -1212,6 +1215,7 @@ fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo] Ok(()) } +// @FIXME #[inline(never)] // avoid stack frame limit fn process_deposit_obligation_collateral( program_id: &Pubkey, @@ -1222,6 +1226,8 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAmount.into()); } + // @FIXME: resume here + let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; @@ -1325,6 +1331,7 @@ fn process_deposit_obligation_collateral( Ok(()) } +// @FIXME #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral( program_id: &Pubkey, diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ef1deacf587..eaeed16e41b 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -136,6 +136,7 @@ impl Obligation { /// Return slots elapsed since last update fn update_slot(&mut self, slot: Slot) -> u64 { + // @TODO: checked math? let slots_elapsed = slot - self.last_update_slot; self.last_update_slot = slot; slots_elapsed diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index dffa99dff25..66266fa5ea7 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -314,6 +314,7 @@ impl Reserve { /// Return slots elapsed since last update fn update_slot(&mut self, slot: Slot) -> u64 { + // @TODO: checked math? let slots_elapsed = slot - self.last_update_slot; self.last_update_slot = slot; slots_elapsed From 43e5eb609698e5051a99ee461917d52b44c4a3df Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:54:27 -0600 Subject: [PATCH 017/191] borrowed_liquidity_wads -> liquidity.borrowed_wads --- token-lending/program/src/state/obligation.rs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index eaeed16e41b..ef53b7016b2 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -71,7 +71,7 @@ impl Obligation { collateral_exchange_rate: CollateralExchangeRate, borrow_token_price: Decimal, ) -> Result { - let loan = self.borrowed_liquidity_wads; + let loan = self.borrowed_wads; let collateral_value = collateral_exchange_rate .decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())? .try_div(borrow_token_price)?; @@ -92,7 +92,7 @@ impl Obligation { /// Liquidate part of obligation pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { - self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?; + self.borrowed_wads = self.borrowed_wads.try_sub(repay_amount)?; self.deposited_collateral_tokens = self .deposited_collateral_tokens .checked_sub(withdraw_amount) @@ -106,14 +106,13 @@ impl Obligation { liquidity_amount: u64, obligation_token_supply: u64, ) -> Result { - let decimal_repay_amount = - Decimal::from(liquidity_amount).min(self.borrowed_liquidity_wads); + let decimal_repay_amount = Decimal::from(liquidity_amount).min(self.borrowed_wads); let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?; if integer_repay_amount == 0 { return Err(LendingError::ObligationEmpty.into()); } - let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?; + let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_wads)?; let collateral_withdraw_amount = { let withdraw_amount: Decimal = repay_pct.try_mul(self.deposited_collateral_tokens)?; withdraw_amount.try_floor_u64()? @@ -278,7 +277,7 @@ mod test { assert_eq!( Obligation { cumulative_borrow_rate_wads: Decimal::one(), - borrowed_liquidity_wads: Decimal::from(u64::MAX), + borrowed_wads: Decimal::from(u64::MAX), ..Obligation::default() } .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), @@ -334,9 +333,9 @@ mod test { (liquidity_amount, borrowed_liquidity) in repay_partial_amounts(), (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), ) { - let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); + let borrowed_wads = Decimal::from_scaled_val(borrowed_liquidity); let mut state = Obligation { - borrowed_liquidity_wads, + borrowed_wads, deposited_collateral_tokens, ..Obligation::default() }; @@ -345,8 +344,8 @@ mod test { assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens); assert!(repay_result.obligation_token_amount < obligation_tokens); - assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads); - assert!(state.borrowed_liquidity_wads > Decimal::zero()); + assert!(state.borrowed_wads < borrowed_wads); + assert!(state.borrowed_wads > Decimal::zero()); assert!(state.deposited_collateral_tokens > 0); let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?; @@ -359,9 +358,9 @@ mod test { (liquidity_amount, borrowed_liquidity) in repay_full_amounts(), (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), ) { - let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); + let borrowed_wads = Decimal::from_scaled_val(borrowed_liquidity); let mut state = Obligation { - borrowed_liquidity_wads, + borrowed_wads, deposited_collateral_tokens, ..Obligation::default() }; @@ -370,8 +369,8 @@ mod test { assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens); assert_eq!(repay_result.obligation_token_amount, obligation_tokens); - assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads); - assert_eq!(state.borrowed_liquidity_wads, Decimal::zero()); + assert_eq!(repay_result.decimal_repay_amount, borrowed_wads); + assert_eq!(state.borrowed_wads, Decimal::zero()); assert_eq!(state.deposited_collateral_tokens, 0); } @@ -380,10 +379,10 @@ mod test { borrowed_liquidity in 0..=u64::MAX, (current_borrow_rate, new_borrow_rate) in cumulative_rates(), ) { - let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity); + let borrowed_wads = Decimal::from(borrowed_liquidity); let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; let mut state = Obligation { - borrowed_liquidity_wads, + borrowed_wads, cumulative_borrow_rate_wads, ..Obligation::default() }; @@ -392,9 +391,9 @@ mod test { state.accrue_interest(next_cumulative_borrow_rate_wads)?; if next_cumulative_borrow_rate_wads > cumulative_borrow_rate_wads { - assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); + assert!(state.borrowed_wads > borrowed_wads); } else { - assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); + assert!(state.borrowed_wads == borrowed_wads); } } } From bf7c38edbf3f943d83c152e73fa99da06f1b8354 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:55:23 -0600 Subject: [PATCH 018/191] processor wip --- token-lending/program/src/processor.rs | 270 +++++++++++++++++++------ 1 file changed, 213 insertions(+), 57 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 4429fb89665..a90af0f70e6 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -6,8 +6,10 @@ use crate::{ instruction::{BorrowAmountType, LendingInstruction}, math::{Decimal, TryAdd, WAD}, state::{ - LendingMarket, LiquidateResult, NewObligationParams, NewReserveParams, Obligation, - RepayResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, + LendingMarket, LiquidateResult, NewObligationCollateralParams, + NewObligationLiquidityParams, NewObligationParams, NewReserveParams, Obligation, + ObligationCollateral, ObligationLiquidity, RepayResult, Reserve, ReserveCollateral, + ReserveConfig, ReserveLiquidity, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, }, }; use num_traits::FromPrimitive; @@ -346,11 +348,12 @@ fn process_init_reserve( Ok(()) } +// @FIXME: what/where should this be? +pub const OBLIGATION_MINT_DECIMALS: u8 = 9; + #[inline(never)] // avoid stack frame limit fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let deposit_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_output_info = next_account_info(account_info_iter)?; @@ -370,47 +373,6 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if borrow_reserve.lending_market != deposit_reserve.lending_market { - return Err(LendingError::LendingMarketMismatch.into()); - } - - if deposit_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } - if deposit_reserve_info.key == borrow_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if deposit_reserve.liquidity.mint_pubkey == borrow_reserve.liquidity.mint_pubkey { - return Err(LendingError::DuplicateReserveMint.into()); - } - - assert_rent_exempt(rent, obligation_info)?; - assert_uninitialized::(obligation_info)?; - assert_last_update_slot(&borrow_reserve, clock.slot)?; - - let cumulative_borrow_rate_wads = borrow_reserve.cumulative_borrow_rate_wads; - let obligation_mint_decimals = deposit_reserve.liquidity.mint_decimals; - let obligation = Obligation::new(NewObligationParams { - collateral_reserve: *deposit_reserve_info.key, - cumulative_borrow_rate_wads: cumulative_borrow_rate_wads, - borrow_reserve: *borrow_reserve_info.key, - token_mint: *obligation_token_mint_info.key, - }); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -421,12 +383,23 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidMarketAuthority.into()); } + assert_rent_exempt(rent, obligation_info)?; + assert_uninitialized::(obligation_info)?; + + let obligation = Obligation::new(NewObligationParams { + collateral: vec![], + liquidity: vec![], + token_mint: obligation_token_mint_info.key(), + current_slot: clock.slot, + }); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + // init obligation token mint spl_token_init_mint(TokenInitializeMintParams { mint: obligation_token_mint_info.clone(), authority: lending_market_authority_info.key, rent: rent_info.clone(), - decimals: obligation_mint_decimals, + decimals: OBLIGATION_MINT_DECIMALS, token_program: token_program_id.clone(), })?; @@ -1233,11 +1206,13 @@ fn process_deposit_obligation_collateral( let destination_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_output_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -1273,11 +1248,6 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.collateral_reserve != deposit_reserve_info.key { - msg!("Invalid deposit reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation.token_mint != obligation_token_mint_info.key { msg!("Obligation token mint input doesn't match existing obligation token mint"); return Err(LendingError::InvalidTokenMint.into()); @@ -1291,12 +1261,21 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidTokenMint.into()); } - obligation.deposited_collateral_tokens = obligation - .deposited_collateral_tokens - .checked_add(collateral_amount) - .ok_or(LendingError::MathOverflow)?; - - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { + msg!("Invalid deposit reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if !obligation + .collateral + .contains(obligation_collateral_info.key) + { + return Err(LendingError::ObligationAccountNotFound.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1308,6 +1287,15 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } + obligation_collateral.deposited_tokens = obligation_collateral + .deposited_tokens + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + // deposit collateral spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), @@ -1556,6 +1544,174 @@ fn process_set_lending_market_owner( Ok(()) } +#[inline(never)] // avoid stack frame limit +fn process_init_obligation_collateral( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent_info = next_account_info(account_info_iter)?; + let rent = &Rent::from_account_info(rent_info)?; + let token_program_id = next_account_info(account_info_iter)?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } + + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &deposit_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if deposit_reserve.config.loan_to_value_ratio == 0 { + return Err(LendingError::ReserveCollateralDisabled.into()); + } + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { + return Err(LendingError::ObligationAccountLimit.into()); + } + if obligation.collateral.contains(deposit_reserve_info.key) { + return Err(LendingError::ObligationAccountDuplicate.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + assert_rent_exempt(rent, obligation_collateral_info)?; + assert_uninitialized::(obligation_collateral_info)?; + + let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { + deposit_reserve: *deposit_reserve_info.key, + current_slot: clock.slot, + }); + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + + obligation.collateral.push(*obligation_collateral_info.key); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut()); + + Ok(()) +} + +#[inline(never)] // avoid stack frame limit +fn process_init_obligation_liquidity( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; + let borrow_reserve_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent_info = next_account_info(account_info_iter)?; + let rent = &Rent::from_account_info(rent_info)?; + let token_program_id = next_account_info(account_info_iter)?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } + + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &borrow_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { + return Err(LendingError::ObligationAccountLimit.into()); + } + if obligation.liquidity.contains(borrow_reserve_info.key) { + return Err(LendingError::ObligationAccountDuplicate.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + assert_rent_exempt(rent, obligation_liquidity_info)?; + assert_uninitialized::(obligation_liquidity_info)?; + + let obligation_liquidity = ObligationLiquidity::new(NewObligationLiquidityParams { + borrow_reserve: *borrow_reserve_info.key, + current_slot: clock.slot, + }); + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; + + obligation.liquidity.push(*obligation_liquidity_info.key); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut()); + + Ok(()) +} + +// @FIXME +fn process_refresh_obligation_collateral( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + Ok(()) +} + +// @FIXME +fn process_refresh_obligation_liquidity( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + Ok(()) +} + +// @FIXME +fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + Ok(()) +} + fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult { if !rent.is_exempt(account_info.lamports(), account_info.data_len()) { msg!(&rent.minimum_balance(account_info.data_len()).to_string()); From 499b4486c5af22045893a1b69db8d9a534964dfd Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:55:55 -0600 Subject: [PATCH 019/191] refactor --- token-lending/program/src/state/obligation.rs | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ef53b7016b2..8cc6b903573 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -166,7 +166,7 @@ impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, OBLIGATION_LEN]; + let output = array_mut_ref![dst, 0, OBLIGATION_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -175,8 +175,17 @@ impl Pack for Obligation { token_mint, num_collateral, num_liquidity, - pubkeys_flat, - ) = mut_array_refs![dst, 1, 8, 16, PUBKEY_LEN, 1, 1, PUBKEY_LEN * MAX_PUBKEYS]; + accounts_flat, + ) = mut_array_refs![ + output, + 1, + 8, + 16, + PUBKEY_LEN, + 1, + 1, + PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS + ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); @@ -190,20 +199,20 @@ impl Pack for Obligation { *num_liquidity = liquidity_len.to_le_bytes(); let mut offset = 0; - for src in self.collateral.iter() { - let dst_array = array_mut_ref![pubkeys_flat, offset, PUBKEY_LEN]; - dst_array.copy_from_slice(src.as_ref()); + for pubkey in self.collateral.iter() { + let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; + account.copy_from_slice(pubkey.as_ref()); offset += PUBKEY_LEN; } - for src in self.liquidity.iter() { - let dst_array = array_mut_ref![pubkeys_flat, offset, PUBKEY_LEN]; - dst_array.copy_from_slice(src.as_ref()); + for pubkey in self.liquidity.iter() { + let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; + account.copy_from_slice(pubkey.as_ref()); offset += PUBKEY_LEN; } } fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, OBLIGATION_LEN]; + let input = array_ref![src, 0, OBLIGATION_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -212,22 +221,32 @@ impl Pack for Obligation { token_mint, num_collateral, num_liquidity, - pubkeys_flat, - ) = array_refs![src, 1, 8, 16, PUBKEY_LEN, 1, 1, PUBKEY_LEN * MAX_PUBKEYS]; - - let collateral_len = u8::from_le_bytes(*num_collateral); - let liquidity_len = u8::from_le_bytes(*num_liquidity); + accounts_flat, + ) = array_refs![ + input, + 1, + 8, + 16, + PUBKEY_LEN, + 1, + 1, + PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS + ]; + + let collateral_len = usize::from_le_bytes(*num_collateral); + let liquidity_len = usize::from_le_bytes(*num_liquidity); let total_len = collateral_len + liquidity_len; - let mut collateral = vec![]; - let mut liquidity = vec![]; + let mut collateral = Vec::with_capacity(collateral_len); + let mut liquidity = Vec::with_capacity(liquidity_len); let mut offset = 0; - for src in pubkeys_flat.chunks(PUBKEY_LEN) { + // @TODO: is there a more idiomatic/performant way to iterate? + for account in accounts_flat.chunks(PUBKEY_LEN) { if offset < collateral_len { - collateral.push(Pubkey::new(src)); + collateral.push(Pubkey::new(account)); } else if offset < total_len { - liquidity.push(Pubkey::new(src)); + liquidity.push(Pubkey::new(account)); } else { break; } From 3bd5d7c372ab3256bbf39648d4e31ef544acd1ac Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 12 Mar 2021 16:56:24 -0600 Subject: [PATCH 020/191] test changes --- token-lending/program/tests/helpers/mod.rs | 8 +++----- token-lending/program/tests/init_obligation.rs | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 9862f4122d7..af80a743e94 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -15,8 +15,8 @@ use spl_token::{ }; use spl_token_lending::{ instruction::{ - borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, init_obligation, - init_reserve, liquidate_obligation, BorrowAmountType, + borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, + init_obligation, init_reserve, liquidate_obligation, BorrowAmountType, }, math::{Decimal, Rate, TryAdd, TryMul}, processor::process_instruction, @@ -1041,13 +1041,11 @@ impl TestObligation { ), init_obligation( spl_token_lending::id(), - deposit_reserve.pubkey, - borrow_reserve.pubkey, - lending_market.pubkey, obligation.pubkey, obligation.token_mint, obligation.token_account, user_accounts_owner.pubkey(), + lending_market.pubkey, ), ], Some(&payer.pubkey()), diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index c1e53af0aaa..ed28a8ffa13 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -131,13 +131,11 @@ async fn test_already_initialized() { let mut transaction = Transaction::new_with_payer( &[init_obligation( spl_token_lending::id(), - sol_reserve.pubkey, - usdc_reserve.pubkey, - lending_market.pubkey, usdc_obligation.pubkey, usdc_obligation.token_mint, usdc_obligation.token_account, user_accounts_owner.pubkey(), + lending_market.pubkey, )], Some(&payer.pubkey()), ); From 897728eb38e65117aeedfa793158a5807a81c25d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:26:19 -0500 Subject: [PATCH 021/191] more errors --- token-lending/program/src/error.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 4503faf78c6..88c52a7b6da 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -85,6 +85,7 @@ pub enum LendingError { /// Input reserves cannot use the same liquidity mint #[error("Input reserves cannot use the same liquidity mint")] DuplicateReserveMint, + // @TODO: make this specific to collateral /// Obligation amount is empty #[error("Obligation amount is empty")] ObligationEmpty, @@ -140,6 +141,16 @@ pub enum LendingError { /// ObligationAccountNotFound #[error("ObligationAccountNotFound")] ObligationAccountNotFound, + // @FIXME: change name + message + #[error("Obligation collateral state needs to be refreshed")] + ObligationCollateralStale, + // @FIXME: change name + message + #[error("Obligation liquidity state needs to be refreshed")] + ObligationLiquidityStale, + // @FIXME: change name + message + #[error("Obligation state needs to be refreshed")] + ObligationStale, + // @FIXME: change name + message /// Invalid obligation collateral amount #[error("Invalid obligation collateral amount")] InvalidObligationCollateral, From 8301a6b79f25ebd8eedb81e40558dead7e7b80af Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:27:12 -0500 Subject: [PATCH 022/191] instruction doc changes + style --- token-lending/program/src/instruction.rs | 251 ++++++++++++----------- 1 file changed, 131 insertions(+), 120 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 47f1ecd4c01..c53ead6123a 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -31,8 +31,8 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Lending market account. - /// 1. `[]` Quote currency SPL Token mint. Must be initialized. + /// 0. `[writable]` Lending market account + /// 1. `[]` Quote currency SPL Token mint /// 2. `[]` Rent sysvar /// 3. `[]` Token program id InitLendingMarket { @@ -45,23 +45,25 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account. $authority can transfer $liquidity_amount + /// 0. `[writable]` Source liquidity token account + /// $authority can transfer $liquidity_amount. /// 1. `[writable]` Destination collateral token account - uninitialized - /// 2. `[writable]` Reserve account. + /// 2. `[writable]` Reserve account /// 3. `[]` Reserve liquidity SPL Token mint /// 4. `[writable]` Reserve liquidity supply SPL Token account - uninitialized /// 5. `[writable]` Reserve collateral SPL Token mint - uninitialized /// 6. `[writable]` Reserve collateral token supply - uninitialized - /// 7. `[writable]` Reserve collateral fees receiver - uninitialized. - /// Owner will be set to the lending market account. - /// 8. `[]` Lending market account. - /// 9. `[signer]` Lending market owner. - /// 10 `[]` Derived lending market authority. - /// 11 `[]` User transfer authority ($authority). + /// 7. `[writable]` Reserve collateral fees receiver - uninitialized + /// 8. `[]` Lending market account + /// 9. `[signer]` Lending market owner + /// 10 `[]` Derived lending market authority + /// 11 `[]` User transfer authority ($authority) /// 12 `[]` Clock sysvar /// 13 `[]` Rent sysvar /// 14 `[]` Token program id - /// 15 `[optional]` Serum DEX market account. Not required for quote currency reserves. Must be initialized and match quote and base currency. + /// 15 `[optional]` Serum DEX market account + /// Not required for quote currency reserves. + /// Must match quote and base currency. InitReserve { /// Initial amount of liquidity to deposit into the new reserve liquidity_amount: u64, @@ -74,15 +76,11 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation - /// 1. `[writable]` Obligation token mint - /// 2. `[writable]` Obligation token output - /// 3. `[]` Obligation token owner - /// 4. `[]` Lending market account. - /// 5. `[]` Derived lending market authority. - /// 6. `[]` Clock sysvar - /// 7. `[]` Rent sysvar - /// 8. `[]` Token program id + /// 0. `[writable]` Obligation account - uninitialized + /// 1. `[]` Lending market account + /// 2. `[]` Clock sysvar + /// 3. `[]` Rent sysvar + /// 4. `[]` Token program id InitObligation, // 3 @@ -91,14 +89,15 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account. $authority can transfer $liquidity_amount - /// 1. `[writable]` Destination collateral token account. - /// 2. `[writable]` Reserve account. - /// 3. `[writable]` Reserve liquidity supply SPL Token account. - /// 4. `[writable]` Reserve collateral SPL Token mint. - /// 5. `[]` Lending market account. - /// 6. `[]` Derived lending market authority. - /// 7. `[signer]` User transfer authority ($authority). + /// 0. `[writable]` Source liquidity token account + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination collateral token account + /// 2. `[writable]` Reserve account + /// 3. `[writable]` Reserve liquidity supply SPL Token account + /// 4. `[writable]` Reserve collateral SPL Token mint + /// 5. `[]` Lending market account + /// 6. `[]` Derived lending market authority + /// 7. `[signer]` User transfer authority ($authority) /// 8. `[]` Clock sysvar /// 9. `[]` Token program id DepositReserveLiquidity { @@ -112,14 +111,15 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source collateral token account. $authority can transfer $collateral_amount - /// 1. `[writable]` Destination liquidity token account. - /// 2. `[writable]` Reserve account. - /// 3. `[writable]` Reserve collateral SPL Token mint. - /// 4. `[writable]` Reserve liquidity supply SPL Token account. - /// 5. `[]` Lending market account. - /// 6. `[]` Derived lending market authority. - /// 7. `[signer]` User transfer authority ($authority). + /// 0. `[writable]` Source collateral token account + /// $authority can transfer $collateral_amount. + /// 1. `[writable]` Destination liquidity token account + /// 2. `[writable]` Reserve account + /// 3. `[writable]` Reserve collateral SPL Token mint + /// 4. `[writable]` Reserve liquidity supply SPL Token account + /// 5. `[]` Lending market account + /// 6. `[]` Derived lending market authority + /// 7. `[signer]` User transfer authority ($authority) /// 8. `[]` Token program id WithdrawReserveLiquidity { /// Amount of collateral to deposit in exchange for liquidity @@ -127,25 +127,27 @@ pub enum LendingInstruction { }, // 5 + // @TODO: update docs /// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens - /// is calculated by market price. The debt obligation is tokenized. Requires a recently - /// refreshed obligation. + /// is calculated by market price. Requires a recently refreshed obligation. Obligation + /// liquidity must be initialized. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account - /// 1. `[writable]` Destination liquidity token account, minted by borrow reserve liquidity mint - /// 2. `[writable]` Borrow reserve account. - /// 3. `[writable]` Borrow reserve liquidity fee receiver account. + /// 1. `[writable]` Destination liquidity token account + /// Minted by borrow reserve liquidity mint. + /// 2. `[writable]` Borrow reserve account + /// 3. `[writable]` Borrow reserve liquidity fee receiver account /// Must be the fee account specified at InitReserve. - /// 4. `[writable]` Obligation - /// 5. `[writable]` Obligation liquidity - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` User transfer authority ($authority). + /// 4. `[writable]` Obligation account + /// 5. `[writable]` Obligation liquidity account + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[signer]` User transfer authority ($authority) /// 9. `[]` Clock sysvar /// 10 `[]` Token program id - /// 11 `[optional, writable]` Borrow reserve liquidity host fee receiver account. + /// 11 `[optional, writable]` Borrow reserve liquidity host fee receiver account BorrowObligationLiquidity { // TODO: slippage constraint /// Amount whose usage depends on `amount_type` @@ -155,46 +157,51 @@ pub enum LendingInstruction { }, // 6 + // @TODO: update docs /// Repay loaned tokens to a reserve. The obligation balance will be recalculated for interest. /// Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account, minted by repay reserve liquidity mint - /// $authority can transfer $liquidity_amount - /// 3. `[writable]` Destination repay reserve liquidity supply SPL Token account - /// 2. `[writable]` Repay reserve account. - /// 6. `[writable]` Obligation - /// 6. `[writable]` Obligation liquidity - /// 9. `[]` Lending market account. - /// 10 `[]` Derived lending market authority. - /// 11 `[signer]` User transfer authority ($authority). - /// 12 `[]` Clock sysvar - /// 13 `[]` Token program id + /// 0. `[writable]` Source liquidity token account + /// Minted by repay reserve liquidity mint. + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account + /// 2. `[writable]` Repay reserve account + /// 3. `[writable]` Obligation account + /// 4. `[writable]` Obligation liquidity account + /// 5. `[]` Lending market account + /// 6. `[]` Derived lending market authority + /// 7. `[signer]` User transfer authority ($authority) + /// 8. `[]` Clock sysvar + /// 9. `[]` Token program id RepayObligationLiquidity { /// Amount of liquidity to repay liquidity_amount: u64, }, // 7 - /// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy. - /// Requires a recently refreshed obligation. + // @TODO: update docs + /// Purchase collateral tokens at a discount rate from an unhealthy obligation. Requires a + /// recently refreshed obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account, minted by repay reserve liquidity mint - /// $authority can transfer $collateral_amount - /// 1. `[writable]` Destination collateral token account, minted by withdraw reserve collateral mint - /// 2. `[writable]` Repay reserve account. + /// 0. `[writable]` Source liquidity token account + /// Minted by repay reserve liquidity mint. + /// $authority can transfer $collateral_amount. + /// 1. `[writable]` Destination collateral token account + /// Minted by withdraw reserve collateral mint + /// 2. `[writable]` Repay reserve account /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[]` Withdraw reserve account. + /// 4. `[]` Withdraw reserve account /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation - /// 6. `[writable]` Obligation collateral - /// 6. `[writable]` Obligation liquidity - /// 7. `[]` Lending market account. - /// 8. `[]` Derived lending market authority. - /// 9. `[signer]` User transfer authority ($authority). + /// 6. `[writable]` Obligation account + /// 6. `[writable]` Obligation collateral account + /// 6. `[writable]` Obligation liquidity account + /// 7. `[]` Lending market account + /// 8. `[]` Derived lending market authority + /// 9. `[signer]` User transfer authority ($authority) /// 12 `[]` Temporary memory /// 13 `[]` Clock sysvar /// 14 `[]` Token program id @@ -209,28 +216,28 @@ pub enum LendingInstruction { /// Accounts expected by this instruction: /// /// 0. `[]` Clock sysvar - /// 1. `[writable]` Reserve account. - /// .. `[writable]` Additional reserve accounts. + /// 1. `[writable]` Reserve account + /// .. `[writable]` Additional reserve accounts AccrueReserveInterest, // 9 - /// Deposit collateral to an obligation. Requires a recently refreshed obligation. + /// Deposit collateral to an obligation. Obligation collateral must be initialized. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source collateral token account, minted by deposit reserve collateral mint, - /// $authority can transfer $collateral_amount + /// 0. `[writable]` Source collateral token account + /// Minted by deposit reserve collateral mint. + /// $authority can transfer $collateral_amount. /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account - /// 2. `[]` Deposit reserve account. - /// 3. `[writable]` Obligation - /// 4. `[writable]` Obligation collateral + /// 2. `[]` Deposit reserve account + /// 3. `[writable]` Obligation account + /// 4. `[writable]` Obligation collateral account /// 5. `[writable]` Obligation token mint - /// 6. `[writable]` Obligation token output - /// 7. `[]` Lending market account. - /// 8. `[]` Derived lending market authority. - /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Clock sysvar - /// 11 `[]` Token program id + /// 6. `[writable]` Obligation token output account + /// 7. `[]` Lending market account + /// 8. `[]` Derived lending market authority + /// 9. `[signer]` User transfer authority ($authority) + /// 10 `[]` Token program id DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, @@ -243,16 +250,17 @@ pub enum LendingInstruction { /// Accounts expected by this instruction: /// /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account - /// 1. `[writable]` Destination collateral token account, minted by withdraw reserve - /// collateral mint. $authority can transfer $collateral_amount - /// 2. `[]` Withdraw reserve account. - /// 3. `[writable]` Obligation - /// 4. `[writable]` Obligation collateral + /// 1. `[writable]` Destination collateral token account + /// Minted by withdraw reserve collateral mint. + /// $authority can transfer $collateral_amount. + /// 2. `[]` Withdraw reserve account + /// 3. `[writable]` Obligation account + /// 4. `[writable]` Obligation collateral account /// 5. `[writable]` Obligation token mint - /// 6. `[writable]` Obligation token input - /// 7. `[]` Lending market account. - /// 8. `[]` Derived lending market authority. - /// 9. `[signer]` User transfer authority ($authority). + /// 6. `[writable]` Obligation token input account + /// 7. `[]` Lending market account + /// 8. `[]` Derived lending market authority + /// 9. `[signer]` User transfer authority ($authority) /// 10 `[]` Clock sysvar /// 11 `[]` Token program id WithdrawObligationCollateral { @@ -265,41 +273,43 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` The lending market to change the owner of. - /// 1. `[signer]` The current owner. + /// 0. `[writable]` Lending market account + /// 1. `[signer]` Current owner SetLendingMarketOwner { /// The new owner new_owner: Pubkey, }, // 12 - /// Initializes a new loan obligation collateral. + /// Initializes a new obligation collateral. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation - /// 1. `[writable]` Obligation collateral - /// 2. `[]` Deposit reserve account. - /// 4. `[]` Lending market account. - /// 5. `[]` Derived lending market authority. - /// 6. `[]` Clock sysvar - /// 7. `[]` Rent sysvar - /// 8. `[]` Token program id + /// 0. `[writable]` Obligation account + /// 1. `[writable]` Obligation collateral account - uninitialized + /// 2. `[]` Deposit reserve account + /// 3. `[writable]` Obligation token mint - uninitialized + /// 4. `[writable]` Obligation token output account + /// 5. `[]` Obligation token owner + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[]` Clock sysvar + /// 9. `[]` Rent sysvar + /// 10 `[]` Token program id InitObligationCollateral, // 13 - /// Initializes a new loan obligation liquidity. + /// Initializes a new obligation liquidity. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation - /// 1. `[writable]` Obligation collateral - /// 2. `[]` Borrow reserve account. - /// 4. `[]` Lending market account. - /// 5. `[]` Derived lending market authority. - /// 6. `[]` Clock sysvar - /// 7. `[]` Rent sysvar - /// 8. `[]` Token program id + /// 0. `[writable]` Obligation account + /// 1. `[writable]` Obligation liquidity account - uninitialized + /// 2. `[]` Borrow reserve account + /// 4. `[]` Lending market account + /// 5. `[]` Clock sysvar + /// 6. `[]` Rent sysvar + /// 7. `[]` Token program id InitObligationLiquidity, // 14 @@ -340,12 +350,13 @@ pub enum LendingInstruction { /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation account - /// 1..1+N `[]` All obligation collateral accounts, followed by obligation liquidity accounts - /// 2+N `[]` Lending market account - /// 3+N `[]` Derived lending market authority - /// 4+N `[]` Temporary memory - /// 5+N `[]` Clock sysvar - /// 6+N `[]` Token program id + /// 1. `[]` Lending market account + /// 2. `[]` Clock sysvar + /// 3. `[]` Token program id + /// 4..4+N `[]` Obligation collateral and liquidity accounts + /// Must be all initialized collateral accounts in exact order, followed by + /// all initialized liquidity accounts in exact order, with no additional + /// accounts following. RefreshObligation, } From e0d4febfb3f0d97b793307abba4304f914e9d284 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:31:20 -0500 Subject: [PATCH 023/191] instruction changes --- token-lending/program/src/instruction.rs | 38 +++++++++--------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index c53ead6123a..76bfe5815ab 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -665,22 +665,13 @@ pub fn init_reserve( pub fn init_obligation( program_id: Pubkey, obligation_pubkey: Pubkey, - obligation_token_mint_pubkey: Pubkey, - obligation_token_output_pubkey: Pubkey, - obligation_token_owner_pubkey: Pubkey, lending_market_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), - AccountMeta::new_readonly(obligation_token_owner_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), @@ -941,7 +932,6 @@ pub fn deposit_obligation_collateral( AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), @@ -1008,6 +998,9 @@ pub fn init_obligation_collateral( obligation_pubkey: Pubkey, obligation_collateral_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, + obligation_token_mint_pubkey: Pubkey, + obligation_token_output_pubkey: Pubkey, + obligation_token_owner_pubkey: Pubkey, lending_market_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = @@ -1018,6 +1011,9 @@ pub fn init_obligation_collateral( AccountMeta::new(obligation_pubkey, false), AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), + AccountMeta::new(obligation_token_mint_pubkey, false), + AccountMeta::new(obligation_token_output_pubkey, false), + AccountMeta::new_readonly(obligation_token_owner_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), @@ -1037,8 +1033,6 @@ pub fn init_obligation_liquidity( borrow_reserve_pubkey: Pubkey, lending_market_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -1046,7 +1040,6 @@ pub fn init_obligation_liquidity( AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), @@ -1120,24 +1113,21 @@ pub fn refresh_obligation_liquidity( pub fn refresh_obligation( program_id: Pubkey, obligation_pubkey: Pubkey, - obligation_collateral_liquidity_pubkeys: Vec, lending_market_pubkey: Pubkey, + obligation_collateral_liquidity_pubkeys: Vec, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); - let mut accounts = Vec::with_capacity(5 + obligation_collateral_liquidity_pubkeys.len()); - accounts.push(AccountMeta::new(obligation_pubkey, false)); - accounts.extend( - obligation_collateral_liquidity_pubkeys - .into_iter() - .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), - ); + let mut accounts = Vec::with_capacity(4 + obligation_collateral_liquidity_pubkeys.len()); accounts.extend(vec![ + AccountMeta::new(obligation_pubkey, false) AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]); + accounts.extend( + obligation_collateral_liquidity_pubkeys + .into_iter() + .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + ); Instruction { program_id, accounts, From 81e64694b99e5da73d56965c88606ad73a7dfbb7 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:39:37 -0500 Subject: [PATCH 024/191] fix init_obligation --- token-lending/program/src/processor.rs | 48 ++++---------------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index a90af0f70e6..3a197d0b037 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -348,23 +348,18 @@ fn process_init_reserve( Ok(()) } -// @FIXME: what/where should this be? -pub const OBLIGATION_MINT_DECIMALS: u8 = 9; - #[inline(never)] // avoid stack frame limit fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; - let obligation_token_owner_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; + let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, obligation_info)?; + assert_uninitialized::(obligation_info)?; + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -373,45 +368,14 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - assert_rent_exempt(rent, obligation_info)?; - assert_uninitialized::(obligation_info)?; - let obligation = Obligation::new(NewObligationParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, collateral: vec![], liquidity: vec![], - token_mint: obligation_token_mint_info.key(), - current_slot: clock.slot, }); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - // init obligation token mint - spl_token_init_mint(TokenInitializeMintParams { - mint: obligation_token_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: OBLIGATION_MINT_DECIMALS, - token_program: token_program_id.clone(), - })?; - - // init obligation token output account - spl_token_init_account(TokenInitializeAccountParams { - account: obligation_token_output_info.clone(), - mint: obligation_token_mint_info.clone(), - owner: obligation_token_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - Ok(()) } From d158fd64eed20d81fee7783a975544201f273094 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:41:04 -0500 Subject: [PATCH 025/191] Token -> Account --- token-lending/program/src/processor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 3a197d0b037..9e848ec250f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -26,6 +26,7 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; +use spl_token::state::Account; /// Processes an instruction pub fn process_instruction( @@ -688,7 +689,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidTokenMint.into()); } - let obligation_token_output = Token::unpack(&obligation_token_output_info.data.borrow())?; + let obligation_token_output = Account::unpack(&obligation_token_output_info.data.borrow())?; if obligation_token_output_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } From c931873459404bb17653b9c11102408f8e38c788 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:42:22 -0500 Subject: [PATCH 026/191] check mint owner --- token-lending/program/src/processor.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 9e848ec250f..6fb1227d607 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -684,6 +684,10 @@ fn process_borrow_obligation_liquidity( } unpack_mint(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } + if &obligation.token_mint != obligation_token_mint_info.key { msg!("Obligation token mint input doesn't match existing obligation token mint"); return Err(LendingError::InvalidTokenMint.into()); @@ -850,6 +854,9 @@ fn process_repay_obligation_liquidity( } let obligation_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } if &obligation.token_mint != obligation_token_mint_info.key { msg!("Invalid obligation token mint account"); return Err(LendingError::InvalidAccountInput.into()); From 88ebed5c2d8daf3df061439eb0eef290b5fd532e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:43:59 -0500 Subject: [PATCH 027/191] add/remove todo comments --- token-lending/program/src/processor.rs | 8 +++++--- token-lending/program/src/state/obligation.rs | 3 +++ token-lending/program/src/state/reserve.rs | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6fb1227d607..a6358dbec80 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -113,6 +113,7 @@ pub fn process_instruction( } } +// @FIXME fn process_init_lending_market( program_id: &Pubkey, market_owner: Pubkey, @@ -145,6 +146,7 @@ fn process_init_lending_market( Ok(()) } +// @FIXME fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, @@ -736,6 +738,7 @@ fn process_borrow_obligation_liquidity( obligation.borrowed_liquidity_wads = obligation .borrowed_liquidity_wads .try_add(Decimal::from(loan.borrow_amount))?; + // @FIXME: unchecked math obligation.deposited_collateral_tokens += loan.collateral_amount; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -755,6 +758,7 @@ fn process_borrow_obligation_liquidity( let mut owner_fee = loan.origination_fee; if let Ok(host_fee_recipient) = next_account_info(account_info_iter) { if loan.host_fee > 0 { + // @FIXME: unchecked math owner_fee -= loan.host_fee; spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), @@ -1160,7 +1164,6 @@ fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo] Ok(()) } -// @FIXME #[inline(never)] // avoid stack frame limit fn process_deposit_obligation_collateral( program_id: &Pubkey, @@ -1171,8 +1174,6 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAmount.into()); } - // @FIXME: resume here - let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; @@ -1295,6 +1296,7 @@ fn process_deposit_obligation_collateral( #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral( program_id: &Pubkey, + // @TODO: allow fixed or max withdraw collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 8cc6b903573..2a49a88c810 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -202,11 +202,13 @@ impl Pack for Obligation { for pubkey in self.collateral.iter() { let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; account.copy_from_slice(pubkey.as_ref()); + // @FIXME: unchecked math offset += PUBKEY_LEN; } for pubkey in self.liquidity.iter() { let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; account.copy_from_slice(pubkey.as_ref()); + // @FIXME: unchecked math offset += PUBKEY_LEN; } } @@ -250,6 +252,7 @@ impl Pack for Obligation { } else { break; } + // @FIXME: unchecked math offset += 1; } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 66266fa5ea7..70efc271c06 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -270,6 +270,7 @@ impl Reserve { let collateral_amount = collateral_exchange_rate.liquidity_to_collateral(liquidity_amount)?; + // @FIXME: unchecked math self.liquidity.available_amount += liquidity_amount; self.collateral.mint_total_supply += collateral_amount; @@ -285,6 +286,7 @@ impl Reserve { return Err(LendingError::InsufficientLiquidity.into()); } + // @FIXME: unchecked math self.liquidity.available_amount -= liquidity_amount; self.collateral.mint_total_supply -= collateral_amount; @@ -378,6 +380,7 @@ pub struct LiquidateResult { } /// Reserve liquidity +// @TODO: track market value in quote currency #[derive(Clone, Debug, Default, PartialEq)] pub struct ReserveLiquidity { /// Reserve liquidity mint address @@ -423,6 +426,7 @@ impl ReserveLiquidity { return Err(LendingError::InsufficientLiquidity.into()); } + // @FIXME: unchecked math self.available_amount -= borrow_amount; self.borrowed_amount_wads = self .borrowed_amount_wads @@ -533,10 +537,12 @@ impl From for Rate { pub struct ReserveConfig { /// Optimal utilization rate as a percent pub optimal_utilization_rate: u8, + // @TODO: does this make sense at the reserve level anymore? /// The ratio of the loan to the value of the collateral as a percent pub loan_to_value_ratio: u8, /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation pub liquidation_bonus: u8, + // @TODO: does this make sense at the reserve level anymore? /// The percent at which an obligation is considered unhealthy pub liquidation_threshold: u8, /// Min borrow APY From 17688fc9d1663709f6de990d27010f8573b71419 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:45:06 -0500 Subject: [PATCH 028/191] fix deposit_obligation_collateral --- token-lending/program/src/processor.rs | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index a6358dbec80..d178818bb61 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1185,7 +1185,6 @@ fn process_deposit_obligation_collateral( let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -1220,29 +1219,28 @@ fn process_deposit_obligation_collateral( if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - - if &obligation.token_mint != obligation_token_mint_info.key { - msg!("Obligation token mint input doesn't match existing obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); - } - - let obligation_token_output = Token::unpack(&obligation_token_output_info.data.borrow())?; - if obligation_token_output_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation_token_output.mint != obligation_token_mint_info.key { - return Err(LendingError::InvalidTokenMint.into()); + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); + return Err(LendingError::InvalidAccountInput.into()); } let mut obligation_collateral = ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_info.owner != program_id { + if obligation_collateral_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } + if &obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { msg!("Invalid deposit reserve account"); return Err(LendingError::InvalidAccountInput.into()); } + if &obligation_collateral.token_mint != obligation_token_mint_info.key { + msg!("Obligation token mint input doesn't match existing obligation token mint"); + return Err(LendingError::InvalidTokenMint.into()); + } if !obligation .collateral .contains(obligation_collateral_info.key) @@ -1250,6 +1248,19 @@ fn process_deposit_obligation_collateral( return Err(LendingError::ObligationAccountNotFound.into()); } + unpack_mint(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } + + let obligation_token_output = Account::unpack(&obligation_token_output_info.data.borrow())?; + if obligation_token_output_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } + if &obligation_token_output.mint != obligation_token_mint_info.key { + return Err(LendingError::InvalidTokenMint.into()); + } + let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -1260,16 +1271,12 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - obligation_collateral.deposited_tokens = obligation_collateral - .deposited_tokens - .checked_add(collateral_amount) - .ok_or(LendingError::MathOverflow)?; + obligation_collateral.deposit(collateral_amount)?; ObligationCollateral::pack( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; - // deposit collateral spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), @@ -1279,7 +1286,6 @@ fn process_deposit_obligation_collateral( token_program: token_program_id.clone(), })?; - // mint obligation tokens to output account spl_token_mint_to(TokenMintToParams { mint: obligation_token_mint_info.clone(), destination: obligation_token_output_info.clone(), From 407f744dab32078b630ffd548236dd2074a5b5ad Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:45:54 -0500 Subject: [PATCH 029/191] fix init_obligation_collateral --- token-lending/program/src/processor.rs | 37 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index d178818bb61..c6215fe37c5 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1533,6 +1533,9 @@ fn process_init_obligation_collateral( let obligation_info = next_account_info(account_info_iter)?; let obligation_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; + let obligation_token_mint_info = next_account_info(account_info_iter)?; + let obligation_token_output_info = next_account_info(account_info_iter)?; + let obligation_token_owner_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; @@ -1540,6 +1543,9 @@ fn process_init_obligation_collateral( let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, obligation_collateral_info)?; + assert_uninitialized::(obligation_collateral_info)?; + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1564,6 +1570,10 @@ fn process_init_obligation_collateral( if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { return Err(LendingError::ObligationAccountLimit.into()); } @@ -1581,20 +1591,35 @@ fn process_init_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - assert_rent_exempt(rent, obligation_collateral_info)?; - assert_uninitialized::(obligation_collateral_info)?; - let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { - deposit_reserve: *deposit_reserve_info.key, current_slot: clock.slot, + obligation: *obligation_info.key, + deposit_reserve: *deposit_reserve_info.key, + token_mint: obligation_token_mint_info.key(), }); + obligation.collateral.push(*obligation_collateral_info.key); + ObligationCollateral::pack( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - obligation.collateral.push(*obligation_collateral_info.key); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut()); + spl_token_init_mint(TokenInitializeMintParams { + mint: obligation_token_mint_info.clone(), + authority: lending_market_authority_info.key, + rent: rent_info.clone(), + decimals: deposit_reserve.liquidity.mint_decimals, + token_program: token_program_id.clone(), + })?; + + spl_token_init_account(TokenInitializeAccountParams { + account: obligation_token_output_info.clone(), + mint: obligation_token_mint_info.clone(), + owner: obligation_token_owner_info.clone(), + rent: rent_info.clone(), + token_program: token_program_id.clone(), + })?; Ok(()) } From b57da0b3b05459cb3a883421c0e33cd2080fad9c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:46:23 -0500 Subject: [PATCH 030/191] fix init_obligation_liquidity --- token-lending/program/src/processor.rs | 30 +++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c6215fe37c5..facc3d87f04 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1634,12 +1634,14 @@ fn process_init_obligation_liquidity( let obligation_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, obligation_liquidity_info)?; + assert_uninitialized::(obligation_liquidity_info)?; + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1661,6 +1663,10 @@ fn process_init_obligation_liquidity( if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { return Err(LendingError::ObligationAccountLimit.into()); } @@ -1668,30 +1674,18 @@ fn process_init_obligation_liquidity( return Err(LendingError::ObligationAccountDuplicate.into()); } - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - assert_rent_exempt(rent, obligation_liquidity_info)?; - assert_uninitialized::(obligation_liquidity_info)?; - let obligation_liquidity = ObligationLiquidity::new(NewObligationLiquidityParams { - borrow_reserve: *borrow_reserve_info.key, current_slot: clock.slot, + obligation: *obligation_info.key, + borrow_reserve: *borrow_reserve_info.key, }); + obligation.liquidity.push(*obligation_liquidity_info.key); + ObligationLiquidity::pack( obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut(), )?; - - obligation.liquidity.push(*obligation_liquidity_info.key); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut()); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } From 334ff8f8a6357722f728868a86667a194f8a95f9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:48:35 -0500 Subject: [PATCH 031/191] add refresh processors --- token-lending/program/src/processor.rs | 242 ++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index facc3d87f04..7d3abf61858 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -4,7 +4,7 @@ use crate::{ dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, instruction::{BorrowAmountType, LendingInstruction}, - math::{Decimal, TryAdd, WAD}, + math::{Decimal, TryAdd, TryMul, WAD}, state::{ LendingMarket, LiquidateResult, NewObligationCollateralParams, NewObligationLiquidityParams, NewObligationParams, NewReserveParams, Obligation, @@ -1690,24 +1690,258 @@ fn process_init_obligation_liquidity( Ok(()) } -// @FIXME fn process_refresh_obligation_collateral( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let dex_market_info = next_account_info(account_info_iter)?; + let dex_market_orders_info = next_account_info(account_info_iter)?; + let memory = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; + + // Ensure memory is owned by this program so that we don't have to zero it out + if memory.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } + + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &deposit_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { + msg!("Invalid deposit reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + // @TODO: is this necessary? + assert_last_update_slot(&deposit_reserve, clock.slot)?; + + let trade_simulator = TradeSimulator::new( + dex_market_info, + dex_market_orders_info, + memory, + &lending_market.quote_token_mint, + // @TODO: check these + &lending_market.quote_token_mint, + &deposit_reserve.liquidity.mint_pubkey, + )?; + + obligation_collateral.update_market_value( + deposit_reserve.collateral_exchange_rate()?, + trade_simulator, + &deposit_reserve.liquidity.mint_pubkey, + )?; + obligation_collateral.update_slot(clock.slot)?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + Ok(()) } -// @FIXME fn process_refresh_obligation_liquidity( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_liquidity_info = next_account_info(account_info_iter)?; + let borrow_reserve_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let dex_market_info = next_account_info(account_info_iter)?; + let dex_market_orders_info = next_account_info(account_info_iter)?; + let memory = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; + + // Ensure memory is owned by this program so that we don't have to zero it out + if memory.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } + + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &borrow_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { + msg!("Invalid borrow reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + // @TODO: is this necessary? + assert_last_update_slot(&borrow_reserve, clock.slot)?; + + let trade_simulator = TradeSimulator::new( + dex_market_info, + dex_market_orders_info, + memory, + &lending_market.quote_token_mint, + // @TODO: check these + &lending_market.quote_token_mint, + &borrow_reserve.liquidity.mint_pubkey, + )?; + + obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + obligation_liquidity + .update_market_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; + obligation_liquidity.update_slot(clock.slot)?; + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; + Ok(()) } -// @FIXME fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let mut collateral_market_value = Decimal::zero(); + for pubkey in obligation.collateral { + let obligation_collateral_info = next_account_info(account_info_iter)?; + if obligation_collateral_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if pubkey != obligation_collateral_info.key { + msg!("Invalid obligation collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + // @FIXME: magic number + if obligation_collateral.slots_elapsed(clock.slot) > 10 { + return Err(LendingError::ObligationCollateralStale.into()); + } + + collateral_market_value = + collateral_market_value.try_add(obligation_collateral.market_value)?; + } + + let mut liquidity_market_value = Decimal::zero(); + for pubkey in obligation.liquidity { + let obligation_liquidity_info = next_account_info(account_info_iter)?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if pubkey != obligation_liquidity_info.key { + msg!("Invalid obligation liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + // @FIXME: magic number + if obligation_liquidity.slots_elapsed(clock.slot) > 10 { + return Err(LendingError::ObligationLiquidityStale.into()); + } + + liquidity_market_value = + liquidity_market_value.try_add(obligation_liquidity.market_value)?; + } + + // @TODO: check this + if account_info_iter.count() > 0 { + msg!("Too many obligation collateral or liquidity accounts"); + return Err(LendingError::InvalidAccountInput.into()); + } + + obligation.update_loan_to_value(liquidity_market_value, collateral_market_value)?; + obligation.update_slot(clock.slot)?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + Ok(()) } From 271ad6b905c65e40c62cf54aa715945becfaf763 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:49:10 -0500 Subject: [PATCH 032/191] withdraw_obligation_collateral wip --- token-lending/program/src/processor.rs | 116 ++++++++++--------------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7d3abf61858..df596ce5311 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1314,24 +1314,16 @@ fn process_withdraw_obligation_collateral( let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_input_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - // Ensure memory is owned by this program so that we don't have to zero it out - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1348,28 +1340,10 @@ fn process_withdraw_obligation_collateral( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if borrow_reserve.lending_market != withdraw_reserve.lending_market { - return Err(LendingError::LendingMarketMismatch.into()); - } - - if withdraw_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } - - if withdraw_reserve_info.key == borrow_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { msg!("Invalid withdraw reserve collateral supply account input"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { msg!( "Cannot use withdraw reserve collateral supply as destination collateral account input" @@ -1377,41 +1351,52 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } - // TODO: handle case when neither reserve is the quote currency - if borrow_reserve.dex_market.is_none() && withdraw_reserve.dex_market.is_none() { - msg!("One reserve must have a dex market"); - return Err(LendingError::InvalidAccountInput.into()); + let obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } - if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); - return Err(LendingError::InvalidAccountInput.into()); - } + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); + return Err(LendingError::InvalidAccountInput.into()); } - if let COption::Some(dex_market_pubkey) = withdraw_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); - return Err(LendingError::InvalidAccountInput.into()); - } + // @FIXME: magic number + if obligation.slots_elapsed(clock.slot) > 10 { + return Err(LendingError::ObligationStale.into()); } - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - - if &obligation.collateral_reserve != withdraw_reserve_info.key { + if &obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { msg!("Invalid withdraw reserve account"); return Err(LendingError::InvalidAccountInput.into()); } - - let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; - if &obligation.token_mint != obligation_token_mint_info.key { + if &obligation_collateral.token_mint != obligation_token_mint_info.key { msg!("Obligation token mint input doesn't match existing obligation token mint"); return Err(LendingError::InvalidTokenMint.into()); } + if !obligation + .collateral + .contains(obligation_collateral_info.key) + { + return Err(LendingError::ObligationAccountNotFound.into()); + } + if obligation.last_update_slot < obligation_collateral.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + + let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } - let obligation_token_input = Token::unpack(&obligation_token_input_info.data.borrow())?; + let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; if obligation_token_input_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } @@ -1429,13 +1414,11 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // accrue interest and update rates - assert_last_update_slot(&borrow_reserve, clock.slot)?; assert_last_update_slot(&withdraw_reserve, clock.slot)?; - obligation.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + // @FIXME: resume here, needs new business logic for calculating change to LTV from withdraw - let obligation_collateral_amount = obligation.deposited_collateral_tokens; + let obligation_collateral_amount = obligation_collateral.deposited_tokens; if obligation_collateral_amount == 0 { return Err(LendingError::ObligationEmpty.into()); } @@ -1443,15 +1426,6 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidObligationCollateral.into()); } - let trade_simulator = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - &borrow_reserve.liquidity.mint_pubkey, - &withdraw_reserve.liquidity.mint_pubkey, - )?; - let required_collateral = withdraw_reserve.required_collateral_for_borrow( obligation.borrowed_liquidity_wads.try_ceil_u64()?, &borrow_reserve.liquidity.mint_pubkey, @@ -1461,20 +1435,19 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationCollateralBelowRequired.into()); } - let remaining_collateral = obligation_collateral_amount - .checked_sub(collateral_amount) - .ok_or(LendingError::MathOverflow)?; + let obligation_token_amount = obligation_collateral + .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; + + let remaining_collateral = obligation_collateral.withdraw(collateral_amount)?; if remaining_collateral < required_collateral { return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); } - let obligation_token_amount = obligation - .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; - - obligation.deposited_collateral_tokens = remaining_collateral; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; - // burn obligation tokens spl_token_burn(TokenBurnParams { mint: obligation_token_mint_info.clone(), source: obligation_token_input_info.clone(), @@ -1484,7 +1457,6 @@ fn process_withdraw_obligation_collateral( token_program: token_program_id.clone(), })?; - // withdraw collateral spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), From 9ef59d6e12bceb781453ea5eae6a1d23f6360df6 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:51:25 -0500 Subject: [PATCH 033/191] obligation/collateral/liquidity state changes --- token-lending/program/src/state/obligation.rs | 119 ++++++++--------- .../src/state/obligation_collateral.rs | 121 +++++++++++++++--- .../program/src/state/obligation_liquidity.rs | 75 +++++++---- 3 files changed, 205 insertions(+), 110 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 2a49a88c810..033e58d989d 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -11,85 +11,60 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; -use std::convert::TryInto; +use std::convert::{TryInto, TryFrom}; -// @TODO: true max is potentially ~16 +// @TODO: rename / relocate; true max is potentially 28 pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { - /// Version of the obligation + /// Version of the struct pub version: u8, + /// Last slot when collateral, liquidity, or loan to value updated + pub last_update_slot: Slot, + /// Lending market address + pub lending_market: Pubkey, + /// Ratio of liquidity market value to collateral market value (weighted average) + pub loan_to_value: Decimal, /// Collateral accounts for the obligation pub collateral: Vec, /// Liquidity accounts for the obligation pub liquidity: Vec, - /// Mint address of the tokens for this obligation - pub token_mint: Pubkey, - /// Ratio of liquidity market value to collateral market value (weighted average) - pub loan_to_value: Decimal, - /// Last slot when collateral, liquidity, or loan to value updated - pub last_update_slot: Slot, } /// Create new obligation pub struct NewObligationParams { + /// Current slot + pub current_slot: Slot, + /// Lending market address + pub lending_market: Pubkey, /// Collateral for the obligation pub collateral: Vec, /// Liquidity for the obligation pub liquidity: Vec, - /// Obligation token mint address - pub token_mint: Pubkey, - /// Current slot - pub current_slot: Slot, } impl Obligation { /// Create new obligation pub fn new(params: NewObligationParams) -> Self { let NewObligationParams { + current_slot, + lending_market, collateral, liquidity, - token_mint, - current_slot, } = params; Self { version: PROGRAM_VERSION, + last_update_slot: current_slot, + lending_market, + loan_to_value: Decimal::zero(), collateral, liquidity, - token_mint, - loan_to_value: Decimal::zero(), - last_update_slot: current_slot, } } - /// Ratio of loan balance to collateral value - pub fn loan_to_value( - &self, - collateral_exchange_rate: CollateralExchangeRate, - borrow_token_price: Decimal, - ) -> Result { - let loan = self.borrowed_wads; - let collateral_value = collateral_exchange_rate - .decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())? - .try_div(borrow_token_price)?; - loan.try_div(collateral_value) - } - - /// Amount of obligation tokens for given collateral - pub fn collateral_to_obligation_token_amount( - &self, - collateral_amount: u64, - obligation_token_supply: u64, - ) -> Result { - let withdraw_pct = - Decimal::from(collateral_amount).try_div(self.deposited_collateral_tokens)?; - let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?; - token_amount.try_floor_u64() - } - /// Liquidate part of obligation pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { self.borrowed_wads = self.borrowed_wads.try_sub(repay_amount)?; @@ -133,12 +108,28 @@ impl Obligation { }) } + pub fn update_loan_to_value( + &mut self, + liquidity_market_value: Decimal, + collateral_market_value: Decimal, + ) -> Result { + self.loan_to_value = liquidity_market_value.try_div(collateral_market_value)?; + Ok(self.loan_to_value) + } + + /// Return slots elapsed + pub fn slots_elapsed(&self, slot: Slot) -> Result { + let slots_elapsed = slot + .checked_sub(self.last_update_slot) + .ok_or(LendingError::MathOverflow)?; + Ok(slots_elapsed) + } + /// Return slots elapsed since last update - fn update_slot(&mut self, slot: Slot) -> u64 { - // @TODO: checked math? - let slots_elapsed = slot - self.last_update_slot; + pub fn update_slot(&mut self, slot: Slot) -> Result { + let slots_elapsed = self.slots_elapsed(slot)?; self.last_update_slot = slot; - slots_elapsed + Ok(slots_elapsed) } } @@ -161,7 +152,7 @@ impl IsInitialized for Obligation { } } -const OBLIGATION_LEN: usize = 379; // 1 + 8 + 16 + 32 + 1 + 1 + (32 * 10) +const OBLIGATION_LEN: usize = 379; // 1 + 8 + 32 + 16 + 1 + 1 + (32 * 10) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -171,8 +162,8 @@ impl Pack for Obligation { let ( version, last_update_slot, + lending_market, loan_to_value, - token_mint, num_collateral, num_liquidity, accounts_flat, @@ -180,8 +171,8 @@ impl Pack for Obligation { output, 1, 8, - 16, PUBKEY_LEN, + 16, 1, 1, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS @@ -189,14 +180,12 @@ impl Pack for Obligation { *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); + lending_market.copy_from_slice(self.lending_market.as_ref()); pack_decimal(self.loan_to_value, loan_to_value); - token_mint.copy_from_slice(self.token_mint.as_ref()); - - let collateral_len = self.collateral.len(); - let liquidity_len = self.liquidity.len(); - *num_collateral = collateral_len.to_le_bytes(); - *num_liquidity = liquidity_len.to_le_bytes(); + // @TODO: this seems clunky, is this correct? + *num_collateral = u8::try_from(self.collateral.len())?.to_le_bytes(); + *num_liquidity = u8::try_from(self.liquidity.len())?.to_le_bytes(); let mut offset = 0; for pubkey in self.collateral.iter() { @@ -219,8 +208,8 @@ impl Pack for Obligation { let ( version, last_update_slot, + lending_market, loan_to_value, - token_mint, num_collateral, num_liquidity, accounts_flat, @@ -228,22 +217,24 @@ impl Pack for Obligation { input, 1, 8, - 16, PUBKEY_LEN, + 16, 1, 1, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS ]; - let collateral_len = usize::from_le_bytes(*num_collateral); - let liquidity_len = usize::from_le_bytes(*num_liquidity); + let collateral_len = u8::from_le_bytes(*num_collateral); + let liquidity_len = u8::from_le_bytes(*num_liquidity); + // @FIXME: unchecked math let total_len = collateral_len + liquidity_len; - let mut collateral = Vec::with_capacity(collateral_len); - let mut liquidity = Vec::with_capacity(liquidity_len); + // @TODO: this seems clunky, is this correct? + let mut collateral = Vec::with_capacity(collateral_len.try_into()?); + let mut liquidity = Vec::with_capacity(liquidity_len.try_into()?); let mut offset = 0; - // @TODO: is there a more idiomatic/performant way to iterate? + // @TODO: is there a more idiomatic/performant way to iterate this? for account in accounts_flat.chunks(PUBKEY_LEN) { if offset < collateral_len { collateral.push(Pubkey::new(account)); @@ -259,8 +250,8 @@ impl Pack for Obligation { Ok(Self { version: u8::from_le_bytes(*version), last_update_slot: u64::from_le_bytes(*last_update_slot), + lending_market: Pubkey::new_from_array(*lending_market), loan_to_value: unpack_decimal(loan_to_value), - token_mint: Pubkey::new_from_array(*token_mint), collateral, liquidity, }) diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index 67b41fee504..d78fe83d62f 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -19,47 +19,106 @@ use solana_program::{ pub struct ObligationCollateral { /// Version of the obligation collateral pub version: u8, + /// Last slot when market value updated + pub last_update_slot: Slot, + /// Obligation the collateral is associated with + pub obligation: Pubkey, /// Reserve which collateral tokens were deposited into pub deposit_reserve: Pubkey, + /// Mint address of the tokens for this obligation collateral + pub token_mint: Pubkey, /// Amount of collateral tokens deposited for an obligation pub deposited_tokens: u64, /// Market value of collateral - pub market_value: u64, - /// Last slot when market value updated - pub last_update_slot: Slot, + pub market_value: Decimal, } /// Create new obligation collateral pub struct NewObligationCollateralParams { - /// Deposit reserve address - pub deposit_reserve: Pubkey, /// Current slot pub current_slot: Slot, + /// Obligation address + pub obligation: Pubkey, + /// Deposit reserve address + pub deposit_reserve: Pubkey, + /// Obligation token mint address + pub token_mint: Pubkey, } impl ObligationCollateral { /// Create new obligation collateral pub fn new(params: NewObligationCollateralParams) -> Self { let NewObligationCollateralParams { - deposit_reserve, current_slot, + obligation, + deposit_reserve, + token_mint, } = params; Self { version: PROGRAM_VERSION, + last_update_slot: current_slot, + obligation, deposit_reserve, + token_mint, deposited_tokens: 0, - market_value: 0, - last_update_slot: current_slot, + market_value: Decimal::zero(), } } + pub fn deposit(&mut self, collateral_amount: u64) -> Result { + self.deposited_tokens = self + .deposited_tokens + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(self.deposited_tokens) + } + + pub fn withdraw(&mut self, collateral_amount: u64) -> Result { + self.deposited_tokens = self + .deposited_tokens + .checked_sub(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(self.deposited_tokens) + } + + pub fn update_market_value( + &mut self, + collateral_exchange_rate: CollateralExchangeRate, + converter: impl TokenConverter, + liquidity_token_mint: &Pubkey, + ) -> Result { + let liquidity_amount = collateral_exchange_rate + .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; + self.market_value = converter.convert(liquidity_amount, liquidity_token_mint)?; + Ok(self.market_value) + } + + /// Return slots elapsed + pub fn slots_elapsed(&self, slot: Slot) -> Result { + let slots_elapsed = slot + .checked_sub(self.last_update_slot) + .ok_or(LendingError::MathOverflow)?; + Ok(slots_elapsed) + } + /// Return slots elapsed since last update - fn update_slot(&mut self, slot: Slot) -> u64 { - // @TODO: checked math? - let slots_elapsed = slot - self.last_update_slot; + pub fn update_slot(&mut self, slot: Slot) -> Result { + let slots_elapsed = self.slots_elapsed(slot)?; self.last_update_slot = slot; - slots_elapsed + Ok(slots_elapsed) + } + + /// Amount of obligation tokens for given collateral + pub fn collateral_to_obligation_token_amount( + &self, + collateral_amount: u64, + obligation_token_supply: u64, + ) -> Result { + let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_tokens)?; + withdraw_pct + .try_mul(obligation_token_supply)? + .try_floor_u64() } } @@ -70,35 +129,55 @@ impl IsInitialized for ObligationCollateral { } } -const OBLIGATION_COLLATERAL_LEN: usize = 185; // 1 + 32 + 8 + 8 + 8 + 128 +const OBLIGATION_COLLATERAL_LEN: usize = 257; // 1 + 32 + 32 + 32 + 8 + 8 + 16 + 128 impl Pack for ObligationCollateral { const LEN: usize = OBLIGATION_COLLATERAL_LEN; fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, OBLIGATION_COLLATERAL_LEN]; - let (version, deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = - mut_array_refs![output, 1, PUBKEY_LEN, 8, 8, 8, 128]; + let ( + version, + last_update_slot, + obligation, + deposit_reserve, + token_mint, + deposited_tokens, + market_value, + _padding, + ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + obligation.copy_from_slice(self.obligation.as_ref()); deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); + token_mint.copy_from_slice(self.token_mint.as_ref()); *deposited_tokens = self.deposited_tokens.to_le_bytes(); - *market_value = self.market_value.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); + pack_decimal(self.market_value, market_value); } /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). fn unpack_from_slice(input: &[u8]) -> Result { let input = array_ref![input, 0, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (version, deposit_reserve, deposited_tokens, market_value, last_update_slot, _padding) = - array_refs![input, 1, PUBKEY_LEN, 8, 8, 8, 128]; + let ( + version, + last_update_slot, + obligation, + deposit_reserve, + token_mint, + deposited_tokens, + market_value, + _padding, + ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), + last_update_slot: u64::from_le_bytes(*last_update_slot), + obligation: Pubkey::new_from_array(*obligation), deposit_reserve: Pubkey::new_from_array(*deposit_reserve), + token_mint: Pubkey::new_from_array(*token_mint), deposited_tokens: u64::from_le_bytes(*deposited_tokens), - market_value: u64::from_le_bytes(*market_value), - last_update_slot: u64::from_le_bytes(*last_update_slot), + market_value: unpack_decimal(market_value), }) } } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 9807d3ee679..377e9e45825 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -19,6 +19,10 @@ use solana_program::{ pub struct ObligationLiquidity { /// Version of the obligation liquidity pub version: u8, + /// Last slot when market value and accrued interest updated + pub last_update_slot: Slot, + /// Obligation the liquidity is associated with + pub obligation: Pubkey, /// Reserve which liquidity tokens were borrowed from pub borrow_reserve: Pubkey, /// Borrow rate used for calculating interest @@ -26,34 +30,36 @@ pub struct ObligationLiquidity { /// Amount of liquidity tokens borrowed for an obligation plus interest pub borrowed_wads: Decimal, /// Market value of liquidity - pub market_value: u64, - /// Last slot when market value and accrued interest updated - pub last_update_slot: Slot, + pub market_value: Decimal, } /// Create new obligation liquidity pub struct NewObligationLiquidityParams { - /// Borrow reserve address - pub borrow_reserve: Pubkey, /// Current slot pub current_slot: Slot, + /// Obligation address + pub obligation: Pubkey, + /// Borrow reserve address + pub borrow_reserve: Pubkey, } impl ObligationLiquidity { /// Create new obligation liquidity pub fn new(params: NewObligationLiquidityParams) -> Self { let NewObligationLiquidityParams { - borrow_reserve, current_slot, + obligation, + borrow_reserve, } = params; Self { version: PROGRAM_VERSION, + last_update_slot: current_slot, + obligation, borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), borrowed_wads: Decimal::zero(), - market_value: 0, - last_update_slot: current_slot, + market_value: Decimal::zero(), } } @@ -69,10 +75,9 @@ impl ObligationLiquidity { /// Maximum amount of loan that can be repaid by liquidators pub fn max_liquidation_amount(&self) -> Result { - Ok(self - .borrowed_wads + self.borrowed_wads .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .try_floor_u64()?) + .try_floor_u64() } /// Accrue interest @@ -86,18 +91,34 @@ impl ObligationLiquidity { .try_into()?; self.borrowed_wads = self.borrowed_wads.try_mul(compounded_interest_rate)?; - self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; Ok(()) } + /// Return updated market value + pub fn update_market_value( + &mut self, + converter: impl TokenConverter, + from_token_mint: &Pubkey, + ) -> Result { + self.market_value = converter.convert(self.borrowed_wads, from_token_mint)?; + Ok(self.market_value) + } + + /// Return slots elapsed + pub fn slots_elapsed(&self, slot: Slot) -> Result { + let slots_elapsed = slot + .checked_sub(self.last_update_slot) + .ok_or(LendingError::MathOverflow)?; + Ok(slots_elapsed) + } + /// Return slots elapsed since last update - fn update_slot(&mut self, slot: Slot) -> u64 { - // @TODO: checked math? - let slots_elapsed = slot - self.last_update_slot; + pub fn update_slot(&mut self, slot: Slot) -> Result { + let slots_elapsed = self.slots_elapsed(slot)?; self.last_update_slot = slot; - slots_elapsed + Ok(slots_elapsed) } } @@ -108,7 +129,7 @@ impl IsInitialized for ObligationLiquidity { } } -const OBLIGATION_LIQUIDITY_LEN: usize = 209; // 1 + 32 + 16 + 16 + 8 + 8 + 128 +const OBLIGATION_LIQUIDITY_LEN: usize = 249; // 1 + 8 + 32 + 32 + 16 + 16 + 16 + 128 impl Pack for ObligationLiquidity { const LEN: usize = OBLIGATION_LIQUIDITY_LEN; @@ -116,23 +137,25 @@ impl Pack for ObligationLiquidity { let output = array_mut_ref![output, 0, OBLIGATION_LIQUIDITY_LEN]; let ( version, + last_update_slot, + obligation, borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, market_value, - last_update_slot, _padding, - ) = mut_array_refs![output, 1, PUBKEY_LEN, 16, 16, 8, 8, 128]; + ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update_slot.to_le_bytes(); + obligation.copy_from_slice(self.obligation.as_ref()); borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); pack_decimal( self.cumulative_borrow_rate_wads, cumulative_borrow_rate_wads, ); pack_decimal(self.borrowed_wads, borrowed_wads); - *market_value = self.market_value.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); + pack_decimal(self.market_value, market_value); } /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). @@ -141,21 +164,23 @@ impl Pack for ObligationLiquidity { #[allow(clippy::ptr_offset_with_cast)] let ( version, + last_update_slot, + obligation, borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, market_value, - last_update_slot, _padding, - ) = array_refs![input, 1, PUBKEY_LEN, 16, 16, 8, 8, 128]; + ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), + last_update_slot: u64::from_le_bytes(*last_update_slot), + obligation: Pubkey::new_from_array(*obligation), borrow_reserve: Pubkey::new_from_array(*borrow_reserve), cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), borrowed_wads: unpack_decimal(borrowed_wads), - market_value: u64::from_le_bytes(*market_value), - last_update_slot: u64::from_le_bytes(*last_update_slot), + market_value: unpack_decimal(market_value), }) } } From e7c703e1300ad85830ab7c7e9e85050dfb3a8593 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 10:52:04 -0500 Subject: [PATCH 034/191] checked math --- token-lending/program/src/state/reserve.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 70efc271c06..d0791c2c765 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -315,11 +315,12 @@ impl Reserve { } /// Return slots elapsed since last update - fn update_slot(&mut self, slot: Slot) -> u64 { - // @TODO: checked math? - let slots_elapsed = slot - self.last_update_slot; + fn update_slot(&mut self, slot: Slot) -> Result { + let slots_elapsed = slot + .checked_sub(self.last_update_slot) + .ok_or(LendingError::MathOverflow)?; self.last_update_slot = slot; - slots_elapsed + Ok(slots_elapsed) } /// Compound current borrow rate over elapsed slots From c67a16c247edc2bdf0799922ef0f4d099550f412 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 12:42:53 -0500 Subject: [PATCH 035/191] stale handling --- token-lending/program/src/instruction.rs | 21 +++------ token-lending/program/src/processor.rs | 43 ++++++++++------- token-lending/program/src/state/obligation.rs | 33 ++++++++----- .../src/state/obligation_collateral.rs | 47 ++++++++++--------- .../program/src/state/obligation_liquidity.rs | 27 ++++++----- 5 files changed, 97 insertions(+), 74 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 76bfe5815ab..8bf351d87aa 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -78,9 +78,8 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Obligation account - uninitialized /// 1. `[]` Lending market account - /// 2. `[]` Clock sysvar - /// 3. `[]` Rent sysvar - /// 4. `[]` Token program id + /// 2. `[]` Rent sysvar + /// 3. `[]` Token program id InitObligation, // 3 @@ -159,7 +158,6 @@ pub enum LendingInstruction { // 6 // @TODO: update docs /// Repay loaned tokens to a reserve. The obligation balance will be recalculated for interest. - /// Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// @@ -293,9 +291,8 @@ pub enum LendingInstruction { /// 5. `[]` Obligation token owner /// 6. `[]` Lending market account /// 7. `[]` Derived lending market authority - /// 8. `[]` Clock sysvar - /// 9. `[]` Rent sysvar - /// 10 `[]` Token program id + /// 8. `[]` Rent sysvar + /// 9. `[]` Token program id InitObligationCollateral, // 13 @@ -306,10 +303,9 @@ pub enum LendingInstruction { /// 0. `[writable]` Obligation account /// 1. `[writable]` Obligation liquidity account - uninitialized /// 2. `[]` Borrow reserve account - /// 4. `[]` Lending market account - /// 5. `[]` Clock sysvar - /// 6. `[]` Rent sysvar - /// 7. `[]` Token program id + /// 3. `[]` Lending market account + /// 4. `[]` Rent sysvar + /// 5. `[]` Token program id InitObligationLiquidity, // 14 @@ -672,7 +668,6 @@ pub fn init_obligation( accounts: vec![ AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], @@ -1016,7 +1011,6 @@ pub fn init_obligation_collateral( AccountMeta::new_readonly(obligation_token_owner_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], @@ -1040,7 +1034,6 @@ pub fn init_obligation_liquidity( AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index df596ce5311..dbdd502402d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -356,7 +356,6 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro let account_info_iter = &mut accounts.iter(); let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -372,7 +371,6 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro } let obligation = Obligation::new(NewObligationParams { - current_slot: clock.slot, lending_market: *lending_market_info.key, collateral: vec![], liquidity: vec![], @@ -1272,10 +1270,14 @@ fn process_deposit_obligation_collateral( } obligation_collateral.deposit(collateral_amount)?; + obligation_collateral.mark_stale(); + obligation.mark_stale(); + ObligationCollateral::pack( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), @@ -1351,7 +1353,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } - let obligation = Obligation::unpack(&obligation_info.data.borrow())?; + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } @@ -1359,8 +1361,7 @@ fn process_withdraw_obligation_collateral( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: magic number - if obligation.slots_elapsed(clock.slot) > 10 { + if obligation.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } @@ -1387,6 +1388,12 @@ fn process_withdraw_obligation_collateral( { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation_collateral.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + // @TODO: is this useful? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel if obligation.last_update_slot < obligation_collateral.last_update_slot { return Err(LendingError::ObligationStale.into()); } @@ -1414,6 +1421,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } + // @TODO: is this necessary? assert_last_update_slot(&withdraw_reserve, clock.slot)?; // @FIXME: resume here, needs new business logic for calculating change to LTV from withdraw @@ -1443,10 +1451,15 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); } + obligation_collateral.withdraw(collateral_amount); + obligation_collateral.mark_stale(); + obligation.mark_stale(); + ObligationCollateral::pack( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { mint: obligation_token_mint_info.clone(), @@ -1510,7 +1523,6 @@ fn process_init_obligation_collateral( let obligation_token_owner_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1564,12 +1576,12 @@ fn process_init_obligation_collateral( } let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { - current_slot: clock.slot, obligation: *obligation_info.key, deposit_reserve: *deposit_reserve_info.key, token_mint: obligation_token_mint_info.key(), }); obligation.collateral.push(*obligation_collateral_info.key); + obligation.mark_stale(); ObligationCollateral::pack( obligation_collateral, @@ -1606,7 +1618,6 @@ fn process_init_obligation_liquidity( let obligation_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1647,11 +1658,11 @@ fn process_init_obligation_liquidity( } let obligation_liquidity = ObligationLiquidity::new(NewObligationLiquidityParams { - current_slot: clock.slot, obligation: *obligation_info.key, borrow_reserve: *borrow_reserve_info.key, }); obligation.liquidity.push(*obligation_liquidity_info.key); + obligation.mark_stale(); ObligationLiquidity::pack( obligation_liquidity, @@ -1719,7 +1730,7 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? + // @TODO: is this necessary? collateral exchange rate can change, but interest doesn't accrue assert_last_update_slot(&deposit_reserve, clock.slot)?; let trade_simulator = TradeSimulator::new( @@ -1742,6 +1753,7 @@ fn process_refresh_obligation_collateral( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; + // @TODO: should we mark the obligation stale here? Ok(()) } @@ -1803,7 +1815,7 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? + // @TODO: is this necessary? what if we accrue interest here if it's not the current slot? assert_last_update_slot(&borrow_reserve, clock.slot)?; let trade_simulator = TradeSimulator::new( @@ -1824,6 +1836,7 @@ fn process_refresh_obligation_liquidity( obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut(), )?; + // @TODO: should we mark the obligation stale here? Ok(()) } @@ -1869,8 +1882,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: magic number - if obligation_collateral.slots_elapsed(clock.slot) > 10 { + if obligation_collateral.is_stale(clock.slot)? { return Err(LendingError::ObligationCollateralStale.into()); } @@ -1895,9 +1907,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: magic number - if obligation_liquidity.slots_elapsed(clock.slot) > 10 { - return Err(LendingError::ObligationLiquidityStale.into()); + if obligation_liquidity.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into());z } liquidity_market_value = diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 033e58d989d..5d44475712c 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -11,17 +11,21 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; -use std::convert::{TryInto, TryFrom}; +use std::convert::{TryFrom, TryInto}; // @TODO: rename / relocate; true max is potentially 28 +/// Max number of collateral and liquidity accounts combined for an obligation pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; +/// Number of slots market values are considered stale after +pub const STALE_AFTER_SLOTS: u64 = 10; + /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { /// Version of the struct pub version: u8, - /// Last slot when collateral, liquidity, or loan to value updated + /// Last slot when loan to value updated; set to 0 if collateral or liquidity changed pub last_update_slot: Slot, /// Lending market address pub lending_market: Pubkey, @@ -35,8 +39,6 @@ pub struct Obligation { /// Create new obligation pub struct NewObligationParams { - /// Current slot - pub current_slot: Slot, /// Lending market address pub lending_market: Pubkey, /// Collateral for the obligation @@ -49,7 +51,6 @@ impl Obligation { /// Create new obligation pub fn new(params: NewObligationParams) -> Self { let NewObligationParams { - current_slot, lending_market, collateral, liquidity, @@ -57,7 +58,7 @@ impl Obligation { Self { version: PROGRAM_VERSION, - last_update_slot: current_slot, + last_update_slot: 0, lending_market, loan_to_value: Decimal::zero(), collateral, @@ -112,9 +113,9 @@ impl Obligation { &mut self, liquidity_market_value: Decimal, collateral_market_value: Decimal, - ) -> Result { + ) -> ProgramResult { self.loan_to_value = liquidity_market_value.try_div(collateral_market_value)?; - Ok(self.loan_to_value) + Ok(()) } /// Return slots elapsed @@ -125,11 +126,19 @@ impl Obligation { Ok(slots_elapsed) } - /// Return slots elapsed since last update - pub fn update_slot(&mut self, slot: Slot) -> Result { - let slots_elapsed = self.slots_elapsed(slot)?; + /// Set last update slot + pub fn update_slot(&mut self, slot: Slot) { self.last_update_slot = slot; - Ok(slots_elapsed) + } + + /// Set last update slot to 0 + pub fn mark_stale(&mut self) { + self.update_slot(0); + } + + /// Check if last update slot is recent + pub fn is_stale(&self, slot: Slot) -> Result { + Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } } diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index d78fe83d62f..b14a582ef13 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -19,7 +19,7 @@ use solana_program::{ pub struct ObligationCollateral { /// Version of the obligation collateral pub version: u8, - /// Last slot when market value updated + /// Last slot when market value updated; set to 0 if deposited tokens changed pub last_update_slot: Slot, /// Obligation the collateral is associated with pub obligation: Pubkey, @@ -35,8 +35,6 @@ pub struct ObligationCollateral { /// Create new obligation collateral pub struct NewObligationCollateralParams { - /// Current slot - pub current_slot: Slot, /// Obligation address pub obligation: Pubkey, /// Deposit reserve address @@ -49,7 +47,6 @@ impl ObligationCollateral { /// Create new obligation collateral pub fn new(params: NewObligationCollateralParams) -> Self { let NewObligationCollateralParams { - current_slot, obligation, deposit_reserve, token_mint, @@ -57,7 +54,7 @@ impl ObligationCollateral { Self { version: PROGRAM_VERSION, - last_update_slot: current_slot, + last_update_slot: 0, obligation, deposit_reserve, token_mint, @@ -87,11 +84,23 @@ impl ObligationCollateral { collateral_exchange_rate: CollateralExchangeRate, converter: impl TokenConverter, liquidity_token_mint: &Pubkey, - ) -> Result { + ) -> ProgramResult { let liquidity_amount = collateral_exchange_rate .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; self.market_value = converter.convert(liquidity_amount, liquidity_token_mint)?; - Ok(self.market_value) + Ok(()) + } + + /// Amount of obligation tokens for given collateral + pub fn collateral_to_obligation_token_amount( + &self, + collateral_amount: u64, + obligation_token_supply: u64, + ) -> Result { + let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_tokens)?; + withdraw_pct + .try_mul(obligation_token_supply)? + .try_floor_u64() } /// Return slots elapsed @@ -102,23 +111,19 @@ impl ObligationCollateral { Ok(slots_elapsed) } - /// Return slots elapsed since last update - pub fn update_slot(&mut self, slot: Slot) -> Result { - let slots_elapsed = self.slots_elapsed(slot)?; + /// Set last update slot + pub fn update_slot(&mut self, slot: Slot) { self.last_update_slot = slot; - Ok(slots_elapsed) } - /// Amount of obligation tokens for given collateral - pub fn collateral_to_obligation_token_amount( - &self, - collateral_amount: u64, - obligation_token_supply: u64, - ) -> Result { - let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_tokens)?; - withdraw_pct - .try_mul(obligation_token_supply)? - .try_floor_u64() + /// Set last update slot to 0 + pub fn mark_stale(&mut self) { + self.update_slot(0); + } + + /// Check if last update slot is recent + pub fn is_stale(&self, slot: Slot) -> Result { + Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 377e9e45825..2c042376658 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -19,7 +19,7 @@ use solana_program::{ pub struct ObligationLiquidity { /// Version of the obligation liquidity pub version: u8, - /// Last slot when market value and accrued interest updated + /// Last slot when market value and accrued interest updated; set to 0 if borrowed wads changed pub last_update_slot: Slot, /// Obligation the liquidity is associated with pub obligation: Pubkey, @@ -35,8 +35,6 @@ pub struct ObligationLiquidity { /// Create new obligation liquidity pub struct NewObligationLiquidityParams { - /// Current slot - pub current_slot: Slot, /// Obligation address pub obligation: Pubkey, /// Borrow reserve address @@ -47,14 +45,13 @@ impl ObligationLiquidity { /// Create new obligation liquidity pub fn new(params: NewObligationLiquidityParams) -> Self { let NewObligationLiquidityParams { - current_slot, obligation, borrow_reserve, } = params; Self { version: PROGRAM_VERSION, - last_update_slot: current_slot, + last_update_slot: 0, obligation, borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), @@ -101,9 +98,9 @@ impl ObligationLiquidity { &mut self, converter: impl TokenConverter, from_token_mint: &Pubkey, - ) -> Result { + ) -> ProgramResult { self.market_value = converter.convert(self.borrowed_wads, from_token_mint)?; - Ok(self.market_value) + Ok(()) } /// Return slots elapsed @@ -114,11 +111,19 @@ impl ObligationLiquidity { Ok(slots_elapsed) } - /// Return slots elapsed since last update - pub fn update_slot(&mut self, slot: Slot) -> Result { - let slots_elapsed = self.slots_elapsed(slot)?; + /// Set last update slot + pub fn update_slot(&mut self, slot: Slot) { self.last_update_slot = slot; - Ok(slots_elapsed) + } + + /// Set last update slot to 0 + pub fn mark_stale(&mut self) { + self.update_slot(0); + } + + /// Check if last update slot is recent + pub fn is_stale(&self, slot: Slot) -> Result { + Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } } From bba100a93978254cdd9c977d520c0ce362b202e2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 13:31:18 -0500 Subject: [PATCH 036/191] move reserve loan_to_value_ratio and liquidation_threshold to lending market --- token-lending/program/src/instruction.rs | 40 +++++++---- token-lending/program/src/processor.rs | 68 ++++++++++++------- .../program/src/state/lending_market.rs | 22 ++++-- token-lending/program/src/state/reserve.rs | 25 ++----- 4 files changed, 92 insertions(+), 63 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 8bf351d87aa..0f1fb718586 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -37,7 +37,11 @@ pub enum LendingInstruction { /// 3. `[]` Token program id InitLendingMarket { /// Owner authority which can add new reserves - market_owner: Pubkey, + owner: Pubkey, + /// The ratio of the loan to the value of the collateral as a percent + loan_to_value_ratio: u8, + /// The percent at which an obligation is considered unhealthy + liquidation_threshold: u8, }, // 1 @@ -364,15 +368,19 @@ impl LendingInstruction { .ok_or(LendingError::InstructionUnpackError)?; Ok(match tag { 0 => { - let (market_owner, _rest) = Self::unpack_pubkey(rest)?; - Self::InitLendingMarket { market_owner } + let (owner, _rest) = Self::unpack_pubkey(rest)?; + let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; + let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; + Self::InitLendingMarket { + owner, + loan_to_value_ratio, + liquidation_threshold, + } } 1 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; - let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; - let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; @@ -382,9 +390,7 @@ impl LendingInstruction { liquidity_amount, config: ReserveConfig { optimal_utilization_rate, - loan_to_value_ratio, liquidation_bonus, - liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, @@ -486,18 +492,22 @@ impl LendingInstruction { pub fn pack(&self) -> Vec { let mut buf = Vec::with_capacity(size_of::()); match *self { - Self::InitLendingMarket { market_owner } => { + Self::InitLendingMarket { + owner, + loan_to_value_ratio, + liquidation_threshold, + } => { buf.push(0); - buf.extend_from_slice(market_owner.as_ref()); + buf.extend_from_slice(owner.as_ref()); + buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); + buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); } Self::InitReserve { liquidity_amount, config: ReserveConfig { optimal_utilization_rate, - loan_to_value_ratio, liquidation_bonus, - liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, @@ -511,9 +521,7 @@ impl LendingInstruction { buf.push(1); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&optimal_utilization_rate.to_le_bytes()); - buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); - buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); buf.extend_from_slice(&min_borrow_rate.to_le_bytes()); buf.extend_from_slice(&optimal_borrow_rate.to_le_bytes()); buf.extend_from_slice(&max_borrow_rate.to_le_bytes()); @@ -585,6 +593,8 @@ impl LendingInstruction { /// Creates an 'InitLendingMarket' instruction. pub fn init_lending_market( program_id: Pubkey, + loan_to_value_ratio: u8, + liquidation_threshold: u8, lending_market_pubkey: Pubkey, lending_market_owner: Pubkey, quote_token_mint: Pubkey, @@ -598,7 +608,9 @@ pub fn init_lending_market( AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::InitLendingMarket { - market_owner: lending_market_owner, + owner: lending_market_owner, + loan_to_value_ratio, + liquidation_threshold, } .pack(), } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index dbdd502402d..f113acc349f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -36,9 +36,19 @@ pub fn process_instruction( ) -> ProgramResult { let instruction = LendingInstruction::unpack(input)?; match instruction { - LendingInstruction::InitLendingMarket { market_owner } => { + LendingInstruction::InitLendingMarket { + owner, + loan_to_value_ratio, + liquidation_threshold, + } => { msg!("Instruction: Init Lending Market"); - process_init_lending_market(program_id, market_owner, accounts) + process_init_lending_market( + program_id, + owner, + loan_to_value_ratio, + liquidation_threshold, + accounts, + ) } LendingInstruction::InitReserve { liquidity_amount, @@ -113,12 +123,24 @@ pub fn process_instruction( } } -// @FIXME fn process_init_lending_market( program_id: &Pubkey, - market_owner: Pubkey, + lending_market_owner: Pubkey, + loan_to_value_ratio: u8, + liquidation_threshold: u8, accounts: &[AccountInfo], ) -> ProgramResult { + if loan_to_value_ratio >= 100 { + msg!("Loan to value ratio must be in range [0, 100)"); + return Err(LendingError::InvalidConfig.into()); + } + if liquidation_threshold <= loan_to_value_ratio + || liquidation_threshold > 100 + { + msg!("Liquidation threshold must be in range (LTV, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + let account_info_iter = &mut accounts.iter(); let lending_market_info = next_account_info(account_info_iter)?; let quote_token_mint_info = next_account_info(account_info_iter)?; @@ -131,17 +153,18 @@ fn process_init_lending_market( } assert_rent_exempt(rent, lending_market_info)?; - let mut new_lending_market: LendingMarket = assert_uninitialized(lending_market_info)?; - let bump_seed = Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1; - new_lending_market.version = PROGRAM_VERSION; - new_lending_market.bump_seed = bump_seed; - new_lending_market.owner = market_owner; - new_lending_market.quote_token_mint = *quote_token_mint_info.key; - new_lending_market.token_program_id = *token_program_id.key; - LendingMarket::pack( - new_lending_market, - &mut lending_market_info.data.borrow_mut(), - )?; + assert_uninitialized(lending_market_info)?; + + let mut lending_market = LendingMarket { + version: PROGRAM_VERSION, + bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, + owner: lending_market_owner, + quote_token_mint: *quote_token_mint_info.key, + token_program_id: *token_program_id.key, + loan_to_value_ratio, + liquidation_threshold, + }; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) } @@ -161,20 +184,10 @@ fn process_init_reserve( msg!("Optimal utilization rate must be in range [0, 100]"); return Err(LendingError::InvalidConfig.into()); } - if config.loan_to_value_ratio >= 100 { - msg!("Loan to value ratio must be in range [0, 100)"); - return Err(LendingError::InvalidConfig.into()); - } if config.liquidation_bonus > 100 { msg!("Liquidation bonus must be in range [0, 100]"); return Err(LendingError::InvalidConfig.into()); } - if config.liquidation_threshold <= config.loan_to_value_ratio - || config.liquidation_threshold > 100 - { - msg!("Liquidation threshold must be in range (LTV, 100]"); - return Err(LendingError::InvalidConfig.into()); - } if config.optimal_borrow_rate < config.min_borrow_rate { msg!("Optimal borrow rate must be >= min borrow rate"); return Err(LendingError::InvalidConfig.into()); @@ -622,6 +635,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::LendingMarketMismatch.into()); } + // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? if deposit_reserve.config.loan_to_value_ratio == 0 { return Err(LendingError::ReserveCollateralDisabled.into()); } @@ -1201,6 +1215,7 @@ fn process_deposit_obligation_collateral( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? if deposit_reserve.config.loan_to_value_ratio == 0 { return Err(LendingError::ReserveCollateralDisabled.into()); } @@ -1546,6 +1561,7 @@ fn process_init_obligation_collateral( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? if deposit_reserve.config.loan_to_value_ratio == 0 { return Err(LendingError::ReserveCollateralDisabled.into()); } @@ -1908,7 +1924,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } if obligation_liquidity.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into());z + return Err(LendingError::ObligationLiquidityStale.into()); } liquidity_market_value = diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 05d91b69ace..5e9cc3cd2fe 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -19,6 +19,12 @@ pub struct LendingMarket { pub quote_token_mint: Pubkey, /// Token program id pub token_program_id: Pubkey, + // @TODO: update doc comment + /// The ratio of the loan to the value of the collateral as a percent + pub loan_to_value_ratio: u8, + // @TODO: update doc comment + /// The percent at which an obligation is considered unhealthy + pub liquidation_threshold: u8, } impl Sealed for LendingMarket {} @@ -28,7 +34,7 @@ impl IsInitialized for LendingMarket { } } -const LENDING_MARKET_LEN: usize = 160; +const LENDING_MARKET_LEN: usize = 160; // 1 + 1 + 32 + 32 + 32 + 1 + 1 + 60 impl Pack for LendingMarket { const LEN: usize = LENDING_MARKET_LEN; @@ -36,8 +42,10 @@ impl Pack for LendingMarket { 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_token_mint, token_program_id, _padding) = - array_refs![input, 1, 1, 32, 32, 32, 62]; + let (version, bump_seed, owner, quote_token_mint, token_program_id, + loan_to_value_ratio, + liquidation_threshold, _padding) = + array_refs![input, 1, 1, 32, 32, 32, 1, 1, 60]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -49,18 +57,24 @@ impl Pack for LendingMarket { owner: Pubkey::new_from_array(*owner), quote_token_mint: Pubkey::new_from_array(*quote_token_mint), token_program_id: Pubkey::new_from_array(*token_program_id), + loan_to_value_ratio: u8::from_le_bytes(*loan_to_value_ratio), + liquidation_threshold: u8::from_le_bytes(*liquidation_threshold), }) } 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_token_mint, token_program_id, _padding) = + let (version, bump_seed, owner, quote_token_mint, token_program_id, + loan_to_value_ratio, + liquidation_threshold, _padding) = mut_array_refs![output, 1, 1, 32, 32, 32, 62]; *version = self.version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); token_program_id.copy_from_slice(self.token_program_id.as_ref()); + *loan_to_value_ratio = self.loan_to_value_ratio.to_le_bytes(); + *liquidation_threshold = self.liquidation_threshold.to_le_bytes(); } } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index d0791c2c765..5937f9d113d 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -120,6 +120,7 @@ impl Reserve { ) -> Result { // Check obligation health let borrow_token_price = token_converter.best_price(liquidity_token_mint)?; + // @FIXME: moved to lending market let liquidation_threshold = Rate::from_percent(collateral_reserve_config.liquidation_threshold); let obligation_loan_to_value = @@ -227,6 +228,7 @@ impl Reserve { }) } + // @FIXME: move/remove this /// Calculate allowed borrow for collateral pub fn allowed_borrow_for_collateral( &self, @@ -246,6 +248,7 @@ impl Reserve { Ok(borrow_amount) } + // @FIXME: move/remove this /// Calculate required collateral for borrow pub fn required_collateral_for_borrow( &self, @@ -538,14 +541,8 @@ impl From for Rate { pub struct ReserveConfig { /// Optimal utilization rate as a percent pub optimal_utilization_rate: u8, - // @TODO: does this make sense at the reserve level anymore? - /// The ratio of the loan to the value of the collateral as a percent - pub loan_to_value_ratio: u8, /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation pub liquidation_bonus: u8, - // @TODO: does this make sense at the reserve level anymore? - /// The percent at which an obligation is considered unhealthy - pub liquidation_threshold: u8, /// Min borrow APY pub min_borrow_rate: u8, /// Optimal (utilization) borrow APY @@ -616,7 +613,7 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 602; +const RESERVE_LEN: usize = 600; impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -636,9 +633,7 @@ impl Pack for Reserve { collateral_supply, dex_market, optimal_utilization_rate, - loan_to_value_ratio, liquidation_bonus, - liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, @@ -650,8 +645,7 @@ impl Pack for Reserve { collateral_mint_supply, __padding, ) = array_refs![ - input, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, - 300 + input, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; Ok(Self { version: u8::from_le_bytes(*version), @@ -674,9 +668,7 @@ impl Pack for Reserve { }, config: ReserveConfig { optimal_utilization_rate: u8::from_le_bytes(*optimal_utilization_rate), - loan_to_value_ratio: u8::from_le_bytes(*loan_to_value_ratio), liquidation_bonus: u8::from_le_bytes(*liquidation_bonus), - liquidation_threshold: u8::from_le_bytes(*liquidation_threshold), min_borrow_rate: u8::from_le_bytes(*min_borrow_rate), optimal_borrow_rate: u8::from_le_bytes(*optimal_borrow_rate), max_borrow_rate: u8::from_le_bytes(*max_borrow_rate), @@ -702,9 +694,7 @@ impl Pack for Reserve { collateral_supply, dex_market, optimal_utilization_rate, - loan_to_value_ratio, liquidation_bonus, - liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, @@ -716,8 +706,7 @@ impl Pack for Reserve { collateral_mint_supply, _padding, ) = mut_array_refs![ - output, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, - 300 + output, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); @@ -743,9 +732,7 @@ impl Pack for Reserve { // config *optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); - *loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes(); *liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); - *liquidation_threshold = self.config.liquidation_threshold.to_le_bytes(); *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); From 7289a1c0420e4bc6554be94502317875ea2cc44e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 13:31:30 -0500 Subject: [PATCH 037/191] use checked math --- token-lending/program/src/state/reserve.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 5937f9d113d..961e645be59 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -269,13 +269,18 @@ impl Reserve { /// Record deposited liquidity and return amount of collateral tokens to mint pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result { - let collateral_exchange_rate = self.collateral_exchange_rate()?; - let collateral_amount = - collateral_exchange_rate.liquidity_to_collateral(liquidity_amount)?; + let collateral_amount = self + .collateral_exchange_rate()? + .liquidity_to_collateral(liquidity_amount)?; - // @FIXME: unchecked math - self.liquidity.available_amount += liquidity_amount; - self.collateral.mint_total_supply += collateral_amount; + self.liquidity + .available_amount + .checked_add(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + self.collateral + .mint_total_supply + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; Ok(collateral_amount) } From ceb999825812fcf976320dd1d1d8d3e37cdfa117 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sun, 14 Mar 2021 13:31:37 -0500 Subject: [PATCH 038/191] catch err --- token-lending/program/src/state/reserve.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 961e645be59..ad02fa4fa81 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -303,7 +303,7 @@ impl Reserve { /// Update borrow rate and accrue interest pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult { - let slots_elapsed = self.update_slot(current_slot); + let slots_elapsed = self.update_slot(current_slot)?; if slots_elapsed > 0 { let current_borrow_rate = self.current_borrow_rate()?; let compounded_interest_rate = @@ -322,6 +322,7 @@ impl Reserve { self.collateral.exchange_rate(total_liquidity) } + // @TODO: method should probably match Obligation's /// Return slots elapsed since last update fn update_slot(&mut self, slot: Slot) -> Result { let slots_elapsed = slot From ee7232e8105c70445d73685093bccf1a4cc43582 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 14:32:05 -0500 Subject: [PATCH 039/191] optimize imports --- token-lending/program/src/math/decimal.rs | 9 +++++---- token-lending/program/src/processor.rs | 4 ++-- token-lending/program/src/state/obligation.rs | 2 +- token-lending/program/src/state/obligation_collateral.rs | 5 ++--- token-lending/program/src/state/obligation_liquidity.rs | 5 ++--- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index 0c4509ae827..a6da35bfdc3 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -12,11 +12,12 @@ #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::manual_range_contains)] -use crate::math::Rate; -use crate::{error::LendingError, math::common::*}; +use crate::{ + error::LendingError, + math::{common::*, Rate}, +}; use solana_program::program_error::ProgramError; -use std::convert::TryFrom; -use std::fmt; +use std::{convert::TryFrom, fmt}; use uint::construct_uint; // U192 with 192 bits consisting of 3 x 64-bit words diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f113acc349f..6b6487a19ef 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -3,8 +3,8 @@ use crate::{ dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, - instruction::{BorrowAmountType, LendingInstruction}, - math::{Decimal, TryAdd, TryMul, WAD}, + instruction::{init_lending_market, BorrowAmountType, LendingInstruction}, + math::{Decimal, TryAdd, TryDiv, TryMul, TrySub, WAD}, state::{ LendingMarket, LiquidateResult, NewObligationCollateralParams, NewObligationLiquidityParams, NewObligationParams, NewReserveParams, Obligation, diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 5d44475712c..ae0237c3f77 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -1,7 +1,7 @@ use super::*; use crate::{ error::LendingError, - math::{Decimal, Rate, TryDiv, TryMul, TrySub}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index b14a582ef13..a46489bd8bb 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -1,9 +1,7 @@ -use std::convert::TryInto; - use super::*; use crate::{ error::LendingError, - math::{Decimal, Rate, TryDiv, TryMul, TrySub}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ @@ -13,6 +11,7 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; +use std::convert::TryInto; /// Obligation collateral state #[derive(Clone, Debug, Default, PartialEq)] diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 2c042376658..f5787eec3fd 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -1,9 +1,7 @@ -use std::convert::TryInto; - use super::*; use crate::{ error::LendingError, - math::{Decimal, Rate, TryDiv, TryMul, TrySub}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ @@ -13,6 +11,7 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; +use std::convert::TryInto; /// Obligation liquidity state #[derive(Clone, Debug, Default, PartialEq)] From 715dbf9fa7d2846b08d3fa14d375e6d80027a492 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 14:40:38 -0500 Subject: [PATCH 040/191] fmt --- token-lending/program/src/processor.rs | 4 +--- .../program/src/state/lending_market.rs | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6b6487a19ef..f216b7cc97b 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -134,9 +134,7 @@ fn process_init_lending_market( msg!("Loan to value ratio must be in range [0, 100)"); return Err(LendingError::InvalidConfig.into()); } - if liquidation_threshold <= loan_to_value_ratio - || liquidation_threshold > 100 - { + if liquidation_threshold <= loan_to_value_ratio || liquidation_threshold > 100 { msg!("Liquidation threshold must be in range (LTV, 100]"); return Err(LendingError::InvalidConfig.into()); } diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 5e9cc3cd2fe..8db04630753 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -42,10 +42,16 @@ impl Pack for LendingMarket { 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_token_mint, token_program_id, + let ( + version, + bump_seed, + owner, + quote_token_mint, + token_program_id, loan_to_value_ratio, - liquidation_threshold, _padding) = - array_refs![input, 1, 1, 32, 32, 32, 1, 1, 60]; + liquidation_threshold, + _padding, + ) = array_refs![input, 1, 1, 32, 32, 32, 1, 1, 60]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -65,10 +71,16 @@ impl Pack for LendingMarket { 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_token_mint, token_program_id, + let ( + version, + bump_seed, + owner, + quote_token_mint, + token_program_id, loan_to_value_ratio, - liquidation_threshold, _padding) = - mut_array_refs![output, 1, 1, 32, 32, 32, 62]; + liquidation_threshold, + _padding, + ) = mut_array_refs![output, 1, 1, 32, 32, 32, 62]; *version = self.version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); From 4f169499869dcf47de0b3d578109bc1d256431ee Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 14:43:43 -0500 Subject: [PATCH 041/191] split up collateral and liquidity market value --- token-lending/program/src/state/obligation.rs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ae0237c3f77..79c612a0815 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -29,8 +29,10 @@ pub struct Obligation { pub last_update_slot: Slot, /// Lending market address pub lending_market: Pubkey, - /// Ratio of liquidity market value to collateral market value (weighted average) - pub loan_to_value: Decimal, + /// Collateral market value + pub collateral_market_value: Decimal, + /// Liquidity market value + pub liquidity_market_value: Decimal, /// Collateral accounts for the obligation pub collateral: Vec, /// Liquidity accounts for the obligation @@ -60,7 +62,8 @@ impl Obligation { version: PROGRAM_VERSION, last_update_slot: 0, lending_market, - loan_to_value: Decimal::zero(), + collateral_market_value: Decimal::zero(), + liquidity_market_value: Decimal::zero(), collateral, liquidity, } @@ -161,7 +164,7 @@ impl IsInitialized for Obligation { } } -const OBLIGATION_LEN: usize = 379; // 1 + 8 + 32 + 16 + 1 + 1 + (32 * 10) +const OBLIGATION_LEN: usize = 395; // 1 + 8 + 32 + 16 + 16 + 1 + 1 + (32 * 10) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -172,7 +175,8 @@ impl Pack for Obligation { version, last_update_slot, lending_market, - loan_to_value, + collateral_market_value, + liquidity_market_value, num_collateral, num_liquidity, accounts_flat, @@ -182,6 +186,7 @@ impl Pack for Obligation { 8, PUBKEY_LEN, 16, + 16, 1, 1, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS @@ -190,7 +195,8 @@ impl Pack for Obligation { *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_decimal(self.loan_to_value, loan_to_value); + pack_decimal(self.collateral_market_value, collateral_market_value); + pack_decimal(self.liquidity_market_value, liquidity_market_value); // @TODO: this seems clunky, is this correct? *num_collateral = u8::try_from(self.collateral.len())?.to_le_bytes(); @@ -218,7 +224,8 @@ impl Pack for Obligation { version, last_update_slot, lending_market, - loan_to_value, + collateral_market_value, + liquidity_market_value, num_collateral, num_liquidity, accounts_flat, @@ -228,6 +235,7 @@ impl Pack for Obligation { 8, PUBKEY_LEN, 16, + 16, 1, 1, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS @@ -260,7 +268,8 @@ impl Pack for Obligation { version: u8::from_le_bytes(*version), last_update_slot: u64::from_le_bytes(*last_update_slot), lending_market: Pubkey::new_from_array(*lending_market), - loan_to_value: unpack_decimal(loan_to_value), + collateral_market_value: unpack_decimal(collateral_market_value), + liquidity_market_value: unpack_decimal(liquidity_market_value), collateral, liquidity, }) From dff790580956f91e1c5c7581aa2d8822af5118fa Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 14:45:20 -0500 Subject: [PATCH 042/191] update comments and method sigs --- token-lending/program/src/processor.rs | 8 +++++--- token-lending/program/src/state/obligation.rs | 17 ++++++++--------- .../src/state/obligation_collateral.rs | 16 ++++++++++------ .../program/src/state/obligation_liquidity.rs | 19 ++++++++++++++++--- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f216b7cc97b..295c97451df 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1404,7 +1404,7 @@ fn process_withdraw_obligation_collateral( if obligation_collateral.is_stale(clock.slot)? { return Err(LendingError::ObligationCollateralStale.into()); } - // @TODO: is this useful? other collateral/liquidity could have been updated that we don't + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel if obligation.last_update_slot < obligation_collateral.last_update_slot { @@ -1767,7 +1767,8 @@ fn process_refresh_obligation_collateral( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; - // @TODO: should we mark the obligation stale here? + // @TODO: should we mark the obligation stale here? could also iteratively update + // obligation.collateral_market_value Ok(()) } @@ -1850,7 +1851,8 @@ fn process_refresh_obligation_liquidity( obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut(), )?; - // @TODO: should we mark the obligation stale here? + // @TODO: should we mark the obligation stale here? could also iteratively update + // obligation.liquidity_market_value Ok(()) } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 79c612a0815..ae1e6568dc4 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -69,6 +69,7 @@ impl Obligation { } } + // @FIXME /// Liquidate part of obligation pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { self.borrowed_wads = self.borrowed_wads.try_sub(repay_amount)?; @@ -79,6 +80,7 @@ impl Obligation { Ok(()) } + // @FIXME /// Repay borrowed tokens pub fn repay( &mut self, @@ -112,16 +114,13 @@ impl Obligation { }) } - pub fn update_loan_to_value( - &mut self, - liquidity_market_value: Decimal, - collateral_market_value: Decimal, - ) -> ProgramResult { - self.loan_to_value = liquidity_market_value.try_div(collateral_market_value)?; - Ok(()) + /// Calculate the ratio of liquidity market value to collateral market value + pub fn loan_to_value(&self) -> Result { + self.liquidity_market_value + .try_div(self.collateral_market_value) } - /// Return slots elapsed + /// Return slots elapsed since given slot pub fn slots_elapsed(&self, slot: Slot) -> Result { let slots_elapsed = slot .checked_sub(self.last_update_slot) @@ -139,7 +138,7 @@ impl Obligation { self.update_slot(0); } - /// Check if last update slot is recent + /// Check if last update slot is too long ago pub fn is_stale(&self, slot: Slot) -> Result { Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index a46489bd8bb..f12180d0600 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -62,22 +62,25 @@ impl ObligationCollateral { } } - pub fn deposit(&mut self, collateral_amount: u64) -> Result { + /// Increase deposited collateral + pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult { self.deposited_tokens = self .deposited_tokens .checked_add(collateral_amount) .ok_or(LendingError::MathOverflow)?; - Ok(self.deposited_tokens) + Ok(()) } - pub fn withdraw(&mut self, collateral_amount: u64) -> Result { + /// Decrease deposited collateral + pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult { self.deposited_tokens = self .deposited_tokens .checked_sub(collateral_amount) .ok_or(LendingError::MathOverflow)?; - Ok(self.deposited_tokens) + Ok(()) } + /// Update market value of collateral pub fn update_market_value( &mut self, collateral_exchange_rate: CollateralExchangeRate, @@ -86,6 +89,7 @@ impl ObligationCollateral { ) -> ProgramResult { let liquidity_amount = collateral_exchange_rate .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; + // @TODO: this may be slow/inaccurate for large amounts depending on dex market self.market_value = converter.convert(liquidity_amount, liquidity_token_mint)?; Ok(()) } @@ -102,7 +106,7 @@ impl ObligationCollateral { .try_floor_u64() } - /// Return slots elapsed + /// Return slots elapsed since given slot pub fn slots_elapsed(&self, slot: Slot) -> Result { let slots_elapsed = slot .checked_sub(self.last_update_slot) @@ -120,7 +124,7 @@ impl ObligationCollateral { self.update_slot(0); } - /// Check if last update slot is recent + /// Check if last update slot is too long ago pub fn is_stale(&self, slot: Slot) -> Result { Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index f5787eec3fd..7abcf6f693d 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -59,6 +59,18 @@ impl ObligationLiquidity { } } + /// Decrease borrowed liquidity + pub fn repay(&mut self, liquidity_amount: u64) -> ProgramResult { + self.borrowed_wads = self.borrowed_wads.try_sub(liquidity_amount.into())?; + Ok(()) + } + + /// Increase borrowed liquidity + pub fn borrow(&mut self, liquidity_amount: u64) -> ProgramResult { + self.borrowed_wads = self.borrowed_wads.try_add(liquidity_amount.into())?; + Ok(()) + } + /// Maximum amount of loan that can be closed out by a liquidator due to the remaining balance /// being too small to be liquidated normally. pub fn max_closeable_amount(&self) -> Result { @@ -92,17 +104,18 @@ impl ObligationLiquidity { Ok(()) } - /// Return updated market value + /// Update market value of liquidity pub fn update_market_value( &mut self, converter: impl TokenConverter, from_token_mint: &Pubkey, ) -> ProgramResult { + // @TODO: this may be slow/inaccurate for large amounts depending on dex market self.market_value = converter.convert(self.borrowed_wads, from_token_mint)?; Ok(()) } - /// Return slots elapsed + /// Return slots elapsed since given slot pub fn slots_elapsed(&self, slot: Slot) -> Result { let slots_elapsed = slot .checked_sub(self.last_update_slot) @@ -120,7 +133,7 @@ impl ObligationLiquidity { self.update_slot(0); } - /// Check if last update slot is recent + /// Check if last update slot is too long ago pub fn is_stale(&self, slot: Slot) -> Result { Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } From 2504dd361f9609e499bca84aee7953b8fc2c9197 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 14:45:52 -0500 Subject: [PATCH 043/191] new business logic for collateral withdraw --- token-lending/program/src/processor.rs | 53 +++++++++++++++----------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 295c97451df..b12efedd850 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1313,7 +1313,6 @@ fn process_deposit_obligation_collateral( Ok(()) } -// @FIXME #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral( program_id: &Pubkey, @@ -1377,6 +1376,11 @@ fn process_withdraw_obligation_collateral( if obligation.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update_slot < withdraw_reserve.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } let mut obligation_collateral = ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; @@ -1434,37 +1438,41 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? - assert_last_update_slot(&withdraw_reserve, clock.slot)?; - - // @FIXME: resume here, needs new business logic for calculating change to LTV from withdraw - - let obligation_collateral_amount = obligation_collateral.deposited_tokens; - if obligation_collateral_amount == 0 { + if obligation_collateral.deposited_tokens == 0 { return Err(LendingError::ObligationEmpty.into()); } - if obligation_collateral_amount < collateral_amount { + if obligation_collateral.deposited_tokens < collateral_amount { return Err(LendingError::InvalidObligationCollateral.into()); } - let required_collateral = withdraw_reserve.required_collateral_for_borrow( - obligation.borrowed_liquidity_wads.try_ceil_u64()?, - &borrow_reserve.liquidity.mint_pubkey, - trade_simulator, - )?; - if obligation_collateral_amount < required_collateral { + let lending_market_ltv = Decimal::from_percent(lending_market.loan_to_value_ratio); + let obligation_ltv = obligation.loan_to_value()?; + if obligation_ltv > lending_market_ltv { return Err(LendingError::ObligationCollateralBelowRequired.into()); } + if obligation_ltv == lending_market_ltv { + return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); + } - let obligation_token_amount = obligation_collateral - .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; - - let remaining_collateral = obligation_collateral.withdraw(collateral_amount)?; - if remaining_collateral < required_collateral { + let required_collateral_market_value = obligation + .liquidity_market_value + .try_div(lending_market_ltv)?; + let collateral_market_value_difference = obligation + .collateral_market_value + .try_sub(required_collateral_market_value)?; + let collateral_amount_pct_of_obligation_collateral_total = + Decimal::from(collateral_amount).try_div(obligation_collateral.deposited_tokens)?; + let collateral_amount_market_value = obligation + .collateral_market_value + .try_mul(collateral_amount_pct_of_obligation_collateral_total)?; + if collateral_amount_market_value > collateral_market_value_difference { return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); } - obligation_collateral.withdraw(collateral_amount); + let obligation_token_amount = obligation_collateral + .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; + + obligation_collateral.withdraw(collateral_amount)?; obligation_collateral.mark_stale(); obligation.mark_stale(); @@ -1937,7 +1945,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - obligation.update_loan_to_value(liquidity_market_value, collateral_market_value)?; + obligation.collateral_market_value = collateral_market_value; + obligation.liquidity_market_value = liquidity_market_value; obligation.update_slot(clock.slot)?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; From ce280919af7d59ee6c1fbc298e7a502a1d0cc7ad Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 16:04:12 -0500 Subject: [PATCH 044/191] fees receiver -> fee receiver; consistent with other fee names --- token-lending/client/src/main.rs | 6 ++--- token-lending/program/src/instruction.rs | 10 ++++---- token-lending/program/src/processor.rs | 14 +++++------ token-lending/program/src/state/reserve.rs | 16 ++++++------ token-lending/program/tests/borrow.rs | 4 +-- token-lending/program/tests/helpers/mod.rs | 28 ++++++++++----------- token-lending/program/tests/init_reserve.rs | 2 +- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/token-lending/client/src/main.rs b/token-lending/client/src/main.rs index f5e60396b3e..7113da27ef9 100644 --- a/token-lending/client/src/main.rs +++ b/token-lending/client/src/main.rs @@ -181,7 +181,7 @@ pub fn create_reserve( let collateral_mint_keypair = Keypair::new(); let collateral_supply_keypair = Keypair::new(); let liquidity_supply_keypair = Keypair::new(); - let liquidity_fees_receiver_keypair = Keypair::new(); + let liquidity_fee_receiver_keypair = Keypair::new(); let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority = Keypair::new(); @@ -221,7 +221,7 @@ pub fn create_reserve( ), create_account( &payer.pubkey(), - &liquidity_fees_receiver_keypair.pubkey(), + &liquidity_fee_receiver_keypair.pubkey(), token_balance, Token::LEN as u64, &spl_token::id(), @@ -283,7 +283,7 @@ pub fn create_reserve( liquidity_supply_keypair.pubkey(), collateral_mint_keypair.pubkey(), collateral_supply_keypair.pubkey(), - liquidity_fees_receiver_keypair.pubkey(), + liquidity_fee_receiver_keypair.pubkey(), lending_market_pubkey, lending_market_owner.pubkey(), user_transfer_authority.pubkey(), diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 0f1fb718586..4f2fc3ebabd 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -55,9 +55,9 @@ pub enum LendingInstruction { /// 2. `[writable]` Reserve account /// 3. `[]` Reserve liquidity SPL Token mint /// 4. `[writable]` Reserve liquidity supply SPL Token account - uninitialized + /// 7. `[writable]` Reserve liquidity fee receiver - uninitialized /// 5. `[writable]` Reserve collateral SPL Token mint - uninitialized /// 6. `[writable]` Reserve collateral token supply - uninitialized - /// 7. `[writable]` Reserve collateral fees receiver - uninitialized /// 8. `[]` Lending market account /// 9. `[signer]` Lending market owner /// 10 `[]` Derived lending market authority @@ -627,7 +627,7 @@ pub fn init_reserve( reserve_pubkey: Pubkey, reserve_liquidity_mint_pubkey: Pubkey, reserve_liquidity_supply_pubkey: Pubkey, - reserve_liquidity_fees_receiver_pubkey: Pubkey, + reserve_liquidity_fee_receiver_pubkey: Pubkey, reserve_collateral_mint_pubkey: Pubkey, reserve_collateral_supply_pubkey: Pubkey, lending_market_pubkey: Pubkey, @@ -643,7 +643,7 @@ pub fn init_reserve( AccountMeta::new(reserve_pubkey, false), AccountMeta::new_readonly(reserve_liquidity_mint_pubkey, false), AccountMeta::new(reserve_liquidity_supply_pubkey, false), - AccountMeta::new(reserve_liquidity_fees_receiver_pubkey, false), + AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(reserve_collateral_mint_pubkey, false), AccountMeta::new(reserve_collateral_supply_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), @@ -763,7 +763,7 @@ pub fn borrow_obligation_liquidity( deposit_reserve_collateral_supply_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, borrow_reserve_liquidity_supply_pubkey: Pubkey, - borrow_reserve_liquidity_fees_receiver_pubkey: Pubkey, + borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, @@ -782,7 +782,7 @@ pub fn borrow_obligation_liquidity( AccountMeta::new(deposit_reserve_collateral_supply_pubkey, false), AccountMeta::new(borrow_reserve_pubkey, false), AccountMeta::new(borrow_reserve_liquidity_supply_pubkey, false), - AccountMeta::new(borrow_reserve_liquidity_fees_receiver_pubkey, false), + AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new(obligation_token_mint_pubkey, false), AccountMeta::new(obligation_token_output_pubkey, false), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b12efedd850..b5bfa58e9ed 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -209,7 +209,7 @@ fn process_init_reserve( let reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_mint_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let reserve_liquidity_fees_receiver_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_collateral_supply_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; @@ -284,7 +284,7 @@ fn process_init_reserve( *reserve_liquidity_mint_info.key, reserve_liquidity_mint.decimals, *reserve_liquidity_supply_info.key, - *reserve_liquidity_fees_receiver_info.key, + *reserve_liquidity_fee_receiver_info.key, ); let reserve_collateral_info = ReserveCollateral::new( *reserve_collateral_mint_info.key, @@ -318,7 +318,7 @@ fn process_init_reserve( })?; spl_token_init_account(TokenInitializeAccountParams { - account: reserve_liquidity_fees_receiver_info.clone(), + account: reserve_liquidity_fee_receiver_info.clone(), mint: reserve_liquidity_mint_info.clone(), owner: lending_market_owner_info.clone(), rent: rent_info.clone(), @@ -590,7 +590,7 @@ fn process_borrow_obligation_liquidity( let deposit_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let borrow_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let borrow_reserve_liquidity_fees_receiver_info = next_account_info(account_info_iter)?; + let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_output_info = next_account_info(account_info_iter)?; @@ -659,8 +659,8 @@ fn process_borrow_obligation_liquidity( msg!("Cannot use borrow reserve liquidity supply as destination account input"); return Err(LendingError::InvalidAccountInput.into()); } - if &borrow_reserve.liquidity.fees_receiver != borrow_reserve_liquidity_fees_receiver_info.key { - msg!("Invalid borrow reserve liquidity fees receiver account"); + if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { + msg!("Invalid borrow reserve liquidity fee receiver account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -785,7 +785,7 @@ fn process_borrow_obligation_liquidity( if owner_fee > 0 { spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), - destination: borrow_reserve_liquidity_fees_receiver_info.clone(), + destination: borrow_reserve_liquidity_fee_receiver_info.clone(), amount: owner_fee, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index ad02fa4fa81..2172d2813ee 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -399,8 +399,8 @@ pub struct ReserveLiquidity { pub mint_decimals: u8, /// Reserve liquidity supply address pub supply_pubkey: Pubkey, - /// Reserve liquidity fees receiver address - pub fees_receiver: Pubkey, + /// Reserve liquidity fee receiver address + pub fee_receiver: Pubkey, /// Reserve liquidity available pub available_amount: u64, /// Reserve liquidity borrowed @@ -413,13 +413,13 @@ impl ReserveLiquidity { mint_pubkey: Pubkey, mint_decimals: u8, supply_pubkey: Pubkey, - fees_receiver: Pubkey, + fee_receiver: Pubkey, ) -> Self { Self { mint_pubkey, mint_decimals, supply_pubkey, - fees_receiver, + fee_receiver, available_amount: 0, borrowed_amount_wads: Decimal::zero(), } @@ -634,7 +634,7 @@ impl Pack for Reserve { liquidity_mint, liquidity_mint_decimals, liquidity_supply, - liquidity_fees_receiver, + liquidity_fee_receiver, collateral_mint, collateral_supply, dex_market, @@ -663,7 +663,7 @@ impl Pack for Reserve { mint_pubkey: Pubkey::new_from_array(*liquidity_mint), mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), supply_pubkey: Pubkey::new_from_array(*liquidity_supply), - fees_receiver: Pubkey::new_from_array(*liquidity_fees_receiver), + fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), available_amount: u64::from_le_bytes(*available_liquidity), borrowed_amount_wads: unpack_decimal(total_borrows), }, @@ -695,7 +695,7 @@ impl Pack for Reserve { liquidity_mint, liquidity_mint_decimals, liquidity_supply, - liquidity_fees_receiver, + liquidity_fee_receiver, collateral_mint, collateral_supply, dex_market, @@ -727,7 +727,7 @@ impl Pack for Reserve { liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); - liquidity_fees_receiver.copy_from_slice(self.collateral.fees_receiver.as_ref()); + liquidity_fee_receiver.copy_from_slice(self.collateral.fee_receiver.as_ref()); *available_liquidity = self.liquidity.available_amount.to_le_bytes(); pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index 8d497045755..9824927c671 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -164,7 +164,7 @@ async fn test_borrow_quote_currency() { assert_eq!(collateral_supply, collateral_deposited - total_fee); let fee_balance = - get_token_balance(&mut banks_client, sol_reserve.liquidity_fees_receiver).await; + get_token_balance(&mut banks_client, sol_reserve.liquidity_fee_receiver).await; assert_eq!(fee_balance, total_fee - host_fee); let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.liquidity_host).await; @@ -321,7 +321,7 @@ async fn test_borrow_base_currency() { assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); let fee_balance = - get_token_balance(&mut banks_client, usdc_reserve.liquidity_fees_receiver).await; + get_token_balance(&mut banks_client, usdc_reserve.liquidity_fee_receiver).await; assert_eq!(fee_balance, total_fee - host_fee); let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.liquidity_host).await; diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index af80a743e94..375ec57d6d9 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -309,9 +309,9 @@ pub fn add_reserve( &spl_token::id(), ); - let liquidity_fees_receiver_pubkey = Pubkey::new_unique(); + let liquidity_fee_receiver_pubkey = Pubkey::new_unique(); test.add_packable_account( - liquidity_fees_receiver_pubkey, + liquidity_fee_receiver_pubkey, u32::MAX as u64, &Token { mint: liquidity_mint_pubkey, @@ -343,7 +343,7 @@ pub fn add_reserve( liquidity_mint_pubkey, liquidity_mint_decimals, liquidity_supply_pubkey, - liquidity_fees_receiver_pubkey, + liquidity_fee_receiver_pubkey, ); let reserve_collateral = ReserveCollateral::new(collateral_mint_pubkey, collateral_supply_pubkey); @@ -411,7 +411,7 @@ pub fn add_reserve( liquidity_mint: liquidity_mint_pubkey, liquidity_mint_decimals, liquidity_supply: liquidity_supply_pubkey, - liquidity_fees_receiver: liquidity_fees_receiver_pubkey, + liquidity_fee_receiver: liquidity_fee_receiver_pubkey, liquidity_host: liquidity_host_pubkey, collateral_mint: collateral_mint_pubkey, collateral_supply: collateral_supply_pubkey, @@ -668,7 +668,7 @@ impl TestLendingMarket { borrow_reserve.user_liquidity_account, deposit_reserve.pubkey, deposit_reserve.collateral_supply, - deposit_reserve.liquidity_fees_receiver, + deposit_reserve.liquidity_fee_receiver, borrow_reserve.pubkey, borrow_reserve.liquidity_supply, self.pubkey, @@ -732,7 +732,7 @@ pub struct TestReserve { pub liquidity_supply: Pubkey, pub collateral_mint: Pubkey, pub collateral_supply: Pubkey, - pub liquidity_fees_receiver: Pubkey, + pub liquidity_fee_receiver: Pubkey, pub liquidity_host: Pubkey, pub user_liquidity_account: Pubkey, pub user_collateral_account: Pubkey, @@ -758,7 +758,7 @@ impl TestReserve { let collateral_mint_keypair = Keypair::new(); let collateral_supply_keypair = Keypair::new(); let liquidity_supply_keypair = Keypair::new(); - let liquidity_fees_receiver_keypair = Keypair::new(); + let liquidity_fee_receiver_keypair = Keypair::new(); let liquidity_host_keypair = Keypair::new(); let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority_keypair = Keypair::new(); @@ -811,7 +811,7 @@ impl TestReserve { ), create_account( &payer.pubkey(), - &liquidity_fees_receiver_keypair.pubkey(), + &liquidity_fee_receiver_keypair.pubkey(), rent.minimum_balance(Token::LEN), Token::LEN as u64, &spl_token::id(), @@ -848,7 +848,7 @@ impl TestReserve { liquidity_supply_keypair.pubkey(), liquidity_mint_pubkey, collateral_supply_keypair.pubkey(), - liquidity_fees_receiver_keypair.pubkey(), + liquidity_fee_receiver_keypair.pubkey(), lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority_keypair.pubkey(), @@ -868,7 +868,7 @@ impl TestReserve { &collateral_mint_keypair, &collateral_supply_keypair, &liquidity_supply_keypair, - &liquidity_fees_receiver_keypair, + &liquidity_fee_receiver_keypair, &liquidity_host_keypair, &user_collateral_token_keypair, &user_transfer_authority_keypair, @@ -887,7 +887,7 @@ impl TestReserve { liquidity_mint: liquidity_mint_pubkey, liquidity_mint_decimals: liquidity_mint.decimals, liquidity_supply: liquidity_supply_keypair.pubkey(), - liquidity_fees_receiver: liquidity_fees_receiver_keypair.pubkey(), + liquidity_fee_receiver: liquidity_fee_receiver_keypair.pubkey(), liquidity_host: liquidity_host_keypair.pubkey(), collateral_mint: collateral_mint_keypair.pubkey(), collateral_supply: collateral_supply_keypair.pubkey(), @@ -916,11 +916,11 @@ impl TestReserve { self.name, self.collateral_supply ); genesis_accounts - .fetch_and_insert(banks_client, self.liquidity_fees_receiver) + .fetch_and_insert(banks_client, self.liquidity_fee_receiver) .await; println!( - "{}_collateral_fees_receiver: {}", - self.name, self.liquidity_fees_receiver + "{}_liquidity_fee_receiver: {}", + self.name, self.liquidity_fee_receiver ); genesis_accounts .fetch_and_insert(banks_client, self.collateral_supply) diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index e213dbbcc62..5d280bff35e 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -119,7 +119,7 @@ async fn test_already_initialized() { usdc_reserve.liquidity_supply, usdc_reserve.collateral_mint, usdc_reserve.collateral_supply, - usdc_reserve.liquidity_fees_receiver, + usdc_reserve.liquidity_fee_receiver, lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority.pubkey(), From b5374861e94409628473a9c55b89460860e0ad98 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 15 Mar 2021 16:43:42 -0500 Subject: [PATCH 045/191] use checked math --- token-lending/program/src/state/reserve.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 2172d2813ee..3490e1f96ab 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -294,9 +294,16 @@ impl Reserve { return Err(LendingError::InsufficientLiquidity.into()); } - // @FIXME: unchecked math - self.liquidity.available_amount -= liquidity_amount; - self.collateral.mint_total_supply -= collateral_amount; + self.liquidity.available_amount = self + .liquidity + .available_amount + .checked_sub(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + self.collateral.mint_total_supply = self + .collateral + .mint_total_supply + .checked_sub(collateral_amount) + .ok_or(LendingError::MathOverflow)?; Ok(liquidity_amount) } @@ -436,8 +443,10 @@ impl ReserveLiquidity { return Err(LendingError::InsufficientLiquidity.into()); } - // @FIXME: unchecked math - self.available_amount -= borrow_amount; + self.available_amount -= self + .available_amount + .checked_sub(borrow_amount) + .ok_or(LendingError::MathOverflow)?; self.borrowed_amount_wads = self .borrowed_amount_wads .try_add(Decimal::from(borrow_amount))?; From 6233b1df7da75721ae11919579524a6daa3a2d0b Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:19:18 -0500 Subject: [PATCH 046/191] more errors --- token-lending/program/src/error.rs | 15 ++++++++++++--- token-lending/program/src/processor.rs | 4 ++-- .../tests/withdraw_obligation_collateral.rs | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 88c52a7b6da..c0314daccd7 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -154,12 +154,21 @@ pub enum LendingError { /// Invalid obligation collateral amount #[error("Invalid obligation collateral amount")] InvalidObligationCollateral, - /// Obligation collateral is already below required amount - #[error("Obligation collateral is already below required amount")] - ObligationCollateralBelowRequired, + // @FIXME: change name + message + /// Obligation LTV is above the reserve LTV + #[error("Obligation LTV is above the reserve LTV")] + ObligationLTVAboveReserveLTV, + // @FIXME: change name + message + /// Obligation LTV cannot go above the reserve LTV + #[error("Obligation LTV cannot go above the reserve LTV")] + ObligationLTVCannotGoAboveReserveLTV, + // @FIXME: change name + message /// Obligation collateral cannot be withdrawn below required amount #[error("Obligation collateral cannot be withdrawn below required amount")] ObligationCollateralWithdrawBelowRequired, + /// Borrow amount too small + #[error("Borrow amount too large")] + BorrowTooLarge, } impl From for ProgramError { diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b5bfa58e9ed..261587008c4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1448,10 +1448,10 @@ fn process_withdraw_obligation_collateral( let lending_market_ltv = Decimal::from_percent(lending_market.loan_to_value_ratio); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv > lending_market_ltv { - return Err(LendingError::ObligationCollateralBelowRequired.into()); + return Err(LendingError::ObligationLTVAboveReserveLTV.into()); } if obligation_ltv == lending_market_ltv { - return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); + return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } let required_collateral_market_value = obligation diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index f638f331e99..258b39c918f 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -329,7 +329,7 @@ async fn test_withdraw_below_required() { TransactionError::InstructionError( 3, InstructionError::Custom( - LendingError::ObligationCollateralWithdrawBelowRequired as u32 + LendingError::ObligationLTVCannotGoAboveReserveLTV as u32 ) ) ); From 17e7e5ccb2af0af76bf8b4190abcf293c1e88383 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:20:04 -0500 Subject: [PATCH 047/191] remove unused imports --- token-lending/program/src/instruction.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 4f2fc3ebabd..c682b6b10b1 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -5,7 +5,6 @@ use crate::{ state::{ReserveConfig, ReserveFees}, }; use num_derive::{FromPrimitive, ToPrimitive}; -use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, From c5892d4c2162603cb7163e470db3a2c531ffdefb Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:26:51 -0500 Subject: [PATCH 048/191] todo / fixme comments --- token-lending/program/src/instruction.rs | 10 +++++++--- token-lending/program/src/state/reserve.rs | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index c682b6b10b1..19b1ac14443 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -86,6 +86,7 @@ pub enum LendingInstruction { InitObligation, // 3 + // @TODO: consider renaming to SwapReserveLiquidityForCollateral /// Deposit liquidity into a reserve. The output is a collateral token representing ownership /// of the reserve liquidity pool. /// @@ -103,11 +104,12 @@ pub enum LendingInstruction { /// 8. `[]` Clock sysvar /// 9. `[]` Token program id DepositReserveLiquidity { - /// Amount to deposit into the reserve + /// Amount of liquidity to deposit in exchange for collateral liquidity_amount: u64, }, // 4 + // @TODO: consider renaming to SwapReserveCollateralForLiquidity /// Withdraw tokens from a reserve. The input is a collateral token representing ownership /// of the reserve liquidity pool. /// @@ -124,7 +126,7 @@ pub enum LendingInstruction { /// 7. `[signer]` User transfer authority ($authority) /// 8. `[]` Token program id WithdrawReserveLiquidity { - /// Amount of collateral to deposit in exchange for liquidity + /// Amount of collateral to return in exchange for liquidity collateral_amount: u64, }, @@ -182,7 +184,7 @@ pub enum LendingInstruction { }, // 7 - // @TODO: update docs + // @FIXME /// Purchase collateral tokens at a discount rate from an unhealthy obligation. Requires a /// recently refreshed obligation. /// @@ -811,6 +813,7 @@ pub fn borrow_obligation_liquidity( } } +// @FIXME /// Creates a `RepayObligationLiquidity` instruction #[allow(clippy::too_many_arguments)] pub fn repay_obligation_liquidity( @@ -851,6 +854,7 @@ pub fn repay_obligation_liquidity( } } +// @FIXME /// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] pub fn liquidate_obligation( diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3490e1f96ab..53431139540 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -92,6 +92,7 @@ impl Reserve { } } + // @FIXME /// Liquidate part of an unhealthy obligation pub fn liquidate_obligation( &self, @@ -110,6 +111,7 @@ impl Reserve { ) } + // @FIXME fn _liquidate_obligation( obligation: &Obligation, liquidity_amount: u64, @@ -329,7 +331,7 @@ impl Reserve { self.collateral.exchange_rate(total_liquidity) } - // @TODO: method should probably match Obligation's + // @FIXME: method should match Obligation::update_slot /// Return slots elapsed since last update fn update_slot(&mut self, slot: Slot) -> Result { let slots_elapsed = slot From c66c4e4a7b4b2298d1c54616c5207d92c3e9538d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:27:31 -0500 Subject: [PATCH 049/191] market_value -> value --- token-lending/program/src/processor.rs | 44 +++++++++---------- token-lending/program/src/state/obligation.rs | 31 +++++++------ .../src/state/obligation_collateral.rs | 18 ++++---- .../program/src/state/obligation_liquidity.rs | 18 ++++---- 4 files changed, 55 insertions(+), 56 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 261587008c4..1b9a8f63ba0 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1454,18 +1454,18 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } - let required_collateral_market_value = obligation - .liquidity_market_value + let required_collateral_value = obligation + .liquidity_value .try_div(lending_market_ltv)?; - let collateral_market_value_difference = obligation - .collateral_market_value - .try_sub(required_collateral_market_value)?; - let collateral_amount_pct_of_obligation_collateral_total = + let collateral_value_difference = obligation + .collateral_value + .try_sub(required_collateral_value)?; + let collateral_amount_pct_of_obligation_collateral_amount = Decimal::from(collateral_amount).try_div(obligation_collateral.deposited_tokens)?; - let collateral_amount_market_value = obligation - .collateral_market_value - .try_mul(collateral_amount_pct_of_obligation_collateral_total)?; - if collateral_amount_market_value > collateral_market_value_difference { + let collateral_amount_value = obligation + .collateral_value + .try_mul(collateral_amount_pct_of_obligation_collateral_amount)?; + if collateral_amount_value > collateral_value_difference { return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); } @@ -1765,7 +1765,7 @@ fn process_refresh_obligation_collateral( &deposit_reserve.liquidity.mint_pubkey, )?; - obligation_collateral.update_market_value( + obligation_collateral.update_value( deposit_reserve.collateral_exchange_rate()?, trade_simulator, &deposit_reserve.liquidity.mint_pubkey, @@ -1776,7 +1776,7 @@ fn process_refresh_obligation_collateral( &mut obligation_collateral_info.data.borrow_mut(), )?; // @TODO: should we mark the obligation stale here? could also iteratively update - // obligation.collateral_market_value + // obligation.collateral_value Ok(()) } @@ -1853,14 +1853,14 @@ fn process_refresh_obligation_liquidity( obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; obligation_liquidity - .update_market_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; + .update_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; obligation_liquidity.update_slot(clock.slot)?; ObligationLiquidity::pack( obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut(), )?; // @TODO: should we mark the obligation stale here? could also iteratively update - // obligation.liquidity_market_value + // obligation.liquidity_value Ok(()) } @@ -1889,7 +1889,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - let mut collateral_market_value = Decimal::zero(); + let mut collateral_value = Decimal::zero(); for pubkey in obligation.collateral { let obligation_collateral_info = next_account_info(account_info_iter)?; if obligation_collateral_info.owner != program_id { @@ -1910,11 +1910,11 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ObligationCollateralStale.into()); } - collateral_market_value = - collateral_market_value.try_add(obligation_collateral.market_value)?; + collateral_value = + collateral_value.try_add(obligation_collateral.value)?; } - let mut liquidity_market_value = Decimal::zero(); + let mut liquidity_value = Decimal::zero(); for pubkey in obligation.liquidity { let obligation_liquidity_info = next_account_info(account_info_iter)?; if obligation_liquidity_info.owner != program_id { @@ -1935,8 +1935,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ObligationLiquidityStale.into()); } - liquidity_market_value = - liquidity_market_value.try_add(obligation_liquidity.market_value)?; + liquidity_value = + liquidity_value.try_add(obligation_liquidity.value)?; } // @TODO: check this @@ -1945,8 +1945,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - obligation.collateral_market_value = collateral_market_value; - obligation.liquidity_market_value = liquidity_market_value; + obligation.collateral_value = collateral_value; + obligation.liquidity_value = liquidity_value; obligation.update_slot(clock.slot)?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ae1e6568dc4..6a73a25118f 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -29,10 +29,10 @@ pub struct Obligation { pub last_update_slot: Slot, /// Lending market address pub lending_market: Pubkey, - /// Collateral market value - pub collateral_market_value: Decimal, - /// Liquidity market value - pub liquidity_market_value: Decimal, + /// Collateral market value in quote currency + pub collateral_value: Decimal, + /// Liquidity market value in quote currency + pub liquidity_value: Decimal, /// Collateral accounts for the obligation pub collateral: Vec, /// Liquidity accounts for the obligation @@ -62,8 +62,8 @@ impl Obligation { version: PROGRAM_VERSION, last_update_slot: 0, lending_market, - collateral_market_value: Decimal::zero(), - liquidity_market_value: Decimal::zero(), + collateral_value: Decimal::zero(), + liquidity_value: Decimal::zero(), collateral, liquidity, } @@ -116,8 +116,7 @@ impl Obligation { /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { - self.liquidity_market_value - .try_div(self.collateral_market_value) + self.liquidity_value.try_div(self.collateral_value) } /// Return slots elapsed since given slot @@ -174,8 +173,8 @@ impl Pack for Obligation { version, last_update_slot, lending_market, - collateral_market_value, - liquidity_market_value, + collateral_value, + liquidity_value, num_collateral, num_liquidity, accounts_flat, @@ -194,8 +193,8 @@ impl Pack for Obligation { *version = self.version.to_le_bytes(); *last_update_slot = self.last_update_slot.to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_decimal(self.collateral_market_value, collateral_market_value); - pack_decimal(self.liquidity_market_value, liquidity_market_value); + pack_decimal(self.collateral_value, collateral_value); + pack_decimal(self.liquidity_value, liquidity_value); // @TODO: this seems clunky, is this correct? *num_collateral = u8::try_from(self.collateral.len())?.to_le_bytes(); @@ -223,8 +222,8 @@ impl Pack for Obligation { version, last_update_slot, lending_market, - collateral_market_value, - liquidity_market_value, + collateral_value, + liquidity_value, num_collateral, num_liquidity, accounts_flat, @@ -267,8 +266,8 @@ impl Pack for Obligation { version: u8::from_le_bytes(*version), last_update_slot: u64::from_le_bytes(*last_update_slot), lending_market: Pubkey::new_from_array(*lending_market), - collateral_market_value: unpack_decimal(collateral_market_value), - liquidity_market_value: unpack_decimal(liquidity_market_value), + collateral_value: unpack_decimal(collateral_value), + liquidity_value: unpack_decimal(liquidity_value), collateral, liquidity, }) diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index f12180d0600..a94be04089c 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -28,8 +28,8 @@ pub struct ObligationCollateral { pub token_mint: Pubkey, /// Amount of collateral tokens deposited for an obligation pub deposited_tokens: u64, - /// Market value of collateral - pub market_value: Decimal, + /// Market value of collateral in quote currency + pub value: Decimal, } /// Create new obligation collateral @@ -58,7 +58,7 @@ impl ObligationCollateral { deposit_reserve, token_mint, deposited_tokens: 0, - market_value: Decimal::zero(), + value: Decimal::zero(), } } @@ -81,7 +81,7 @@ impl ObligationCollateral { } /// Update market value of collateral - pub fn update_market_value( + pub fn update_value( &mut self, collateral_exchange_rate: CollateralExchangeRate, converter: impl TokenConverter, @@ -90,7 +90,7 @@ impl ObligationCollateral { let liquidity_amount = collateral_exchange_rate .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.market_value = converter.convert(liquidity_amount, liquidity_token_mint)?; + self.value = converter.convert(liquidity_amount, liquidity_token_mint)?; Ok(()) } @@ -150,7 +150,7 @@ impl Pack for ObligationCollateral { deposit_reserve, token_mint, deposited_tokens, - market_value, + value, _padding, ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; @@ -160,7 +160,7 @@ impl Pack for ObligationCollateral { deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); token_mint.copy_from_slice(self.token_mint.as_ref()); *deposited_tokens = self.deposited_tokens.to_le_bytes(); - pack_decimal(self.market_value, market_value); + pack_decimal(self.value, value); } /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). @@ -174,7 +174,7 @@ impl Pack for ObligationCollateral { deposit_reserve, token_mint, deposited_tokens, - market_value, + value, _padding, ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; @@ -185,7 +185,7 @@ impl Pack for ObligationCollateral { deposit_reserve: Pubkey::new_from_array(*deposit_reserve), token_mint: Pubkey::new_from_array(*token_mint), deposited_tokens: u64::from_le_bytes(*deposited_tokens), - market_value: unpack_decimal(market_value), + value: unpack_decimal(value), }) } } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 7abcf6f693d..118b4bf6a94 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -28,8 +28,8 @@ pub struct ObligationLiquidity { pub cumulative_borrow_rate_wads: Decimal, /// Amount of liquidity tokens borrowed for an obligation plus interest pub borrowed_wads: Decimal, - /// Market value of liquidity - pub market_value: Decimal, + /// Market value of liquidity in quote currency + pub value: Decimal, } /// Create new obligation liquidity @@ -55,7 +55,7 @@ impl ObligationLiquidity { borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), borrowed_wads: Decimal::zero(), - market_value: Decimal::zero(), + value: Decimal::zero(), } } @@ -105,13 +105,13 @@ impl ObligationLiquidity { } /// Update market value of liquidity - pub fn update_market_value( + pub fn update_value( &mut self, converter: impl TokenConverter, from_token_mint: &Pubkey, ) -> ProgramResult { // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.market_value = converter.convert(self.borrowed_wads, from_token_mint)?; + self.value = converter.convert(self.borrowed_wads, from_token_mint)?; Ok(()) } @@ -159,7 +159,7 @@ impl Pack for ObligationLiquidity { borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, - market_value, + value, _padding, ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; @@ -172,7 +172,7 @@ impl Pack for ObligationLiquidity { cumulative_borrow_rate_wads, ); pack_decimal(self.borrowed_wads, borrowed_wads); - pack_decimal(self.market_value, market_value); + pack_decimal(self.value, value); } /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). @@ -186,7 +186,7 @@ impl Pack for ObligationLiquidity { borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, - market_value, + value, _padding, ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; @@ -197,7 +197,7 @@ impl Pack for ObligationLiquidity { borrow_reserve: Pubkey::new_from_array(*borrow_reserve), cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), borrowed_wads: unpack_decimal(borrowed_wads), - market_value: unpack_decimal(market_value), + value: unpack_decimal(value), }) } } From 61f3a3a19c1baa66dc4622c4ffdc94e107bf681c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:29:43 -0500 Subject: [PATCH 050/191] derive lending market authority where possible --- token-lending/program/src/instruction.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 19b1ac14443..6a36357f9eb 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -699,9 +699,10 @@ pub fn deposit_reserve_liquidity( reserve_liquidity_supply_pubkey: Pubkey, reserve_collateral_mint_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -731,9 +732,10 @@ pub fn withdraw_reserve_liquidity( reserve_collateral_mint_pubkey: Pubkey, reserve_liquidity_supply_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -829,9 +831,10 @@ pub fn repay_obligation_liquidity( obligation_mint_pubkey: Pubkey, obligation_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -868,12 +871,13 @@ pub fn liquidate_obligation( withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, dex_market_pubkey: Pubkey, dex_market_order_book_side_pubkey: Pubkey, memory_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -926,9 +930,10 @@ pub fn deposit_obligation_collateral( obligation_mint_pubkey: Pubkey, obligation_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ @@ -961,9 +966,10 @@ pub fn withdraw_obligation_collateral( obligation_mint_pubkey: Pubkey, obligation_input_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ From 26431efe04700771250fe72525f76b634ca0bc47 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:32:16 -0500 Subject: [PATCH 051/191] use rate for LTV --- token-lending/program/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 1b9a8f63ba0..ae856bfc702 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1445,7 +1445,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidObligationCollateral.into()); } - let lending_market_ltv = Decimal::from_percent(lending_market.loan_to_value_ratio); + let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv > lending_market_ltv { return Err(LendingError::ObligationLTVAboveReserveLTV.into()); From 06b380c259f11a67c1076c7c4f3bf0e3b4320b41 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:33:14 -0500 Subject: [PATCH 052/191] check dex market validity --- token-lending/program/src/processor.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ae856bfc702..413489dce49 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1731,6 +1731,16 @@ fn process_refresh_obligation_collateral( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if deposit_reserve.dex_market.is_none() { + msg!("Deposit reserve must have a dex market"); + return Err(LendingError::InvalidAccountInput.into()); + } + if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { + if &dex_market_pubkey != dex_market_info.key { + msg!("Invalid dex market account input"); + return Err(LendingError::InvalidAccountInput.into()); + } + } let mut obligation_collateral = ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; @@ -1817,6 +1827,16 @@ fn process_refresh_obligation_liquidity( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if borrow_reserve.dex_market.is_none() { + msg!("Borrow reserve must have a dex market"); + return Err(LendingError::InvalidAccountInput.into()); + } + if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { + if &dex_market_pubkey != dex_market_info.key { + msg!("Invalid dex market account input"); + return Err(LendingError::InvalidAccountInput.into()); + } + } let mut obligation_liquidity = ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; From ffcfb211f7b034a60a9b46fd603d231edb47cddc Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:23:34 -0500 Subject: [PATCH 053/191] borrow implementation --- token-lending/program/src/instruction.rs | 86 +++---- token-lending/program/src/processor.rs | 230 ++++++++---------- token-lending/program/src/state/reserve.rs | 169 ++++++------- token-lending/program/tests/borrow.rs | 10 +- .../program/tests/genesis_accounts.rs | 10 +- token-lending/program/tests/helpers/mod.rs | 6 +- 6 files changed, 227 insertions(+), 284 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 6a36357f9eb..f744d840330 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -15,11 +15,11 @@ use std::{convert::TryInto, mem::size_of}; /// Describe how the borrow input amount should be treated #[derive(Clone, Copy, Debug, PartialEq, FromPrimitive, ToPrimitive)] -pub enum BorrowAmountType { - /// Treat amount as amount of liquidity to borrow - LiquidityBorrowAmount, - /// Treat amount as amount of collateral tokens to deposit - CollateralDepositAmount, +pub enum AmountType { + /// Treat amount as an exact amount of tokens + ExactAmount, + /// Treat amount as a percentage of available tokens + PercentAmount, } /// Instructions supported by the lending program. @@ -131,7 +131,7 @@ pub enum LendingInstruction { }, // 5 - // @TODO: update docs + // @FIXME: update description /// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens /// is calculated by market price. Requires a recently refreshed obligation. Obligation /// liquidity must be initialized. @@ -148,16 +148,18 @@ pub enum LendingInstruction { /// 5. `[writable]` Obligation liquidity account /// 6. `[]` Lending market account /// 7. `[]` Derived lending market authority - /// 8. `[signer]` User transfer authority ($authority) - /// 9. `[]` Clock sysvar - /// 10 `[]` Token program id - /// 11 `[optional, writable]` Borrow reserve liquidity host fee receiver account + /// 8. `[]` Dex market + /// 9. `[]` Dex market order book side + /// 10 `[]` Temporary memory + /// 11 `[]` Clock sysvar + /// 12 `[]` Token program id + /// 13 `[optional, writable]` Host fee receiver account BorrowObligationLiquidity { // TODO: slippage constraint - /// Amount whose usage depends on `amount_type` - amount: u64, - /// Describe how the amount should be treated - amount_type: BorrowAmountType, + /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` + liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, }, // 6 @@ -414,11 +416,11 @@ impl LendingInstruction { 5 => { let (amount, rest) = Self::unpack_u64(rest)?; let (amount_type, _rest) = Self::unpack_u8(rest)?; - let amount_type = BorrowAmountType::from_u8(amount_type) - .ok_or(LendingError::InstructionUnpackError)?; + let amount_type = + AmountType::from_u8(amount_type).ok_or(LendingError::InstructionUnpackError)?; Self::BorrowObligationLiquidity { - amount, - amount_type, + liquidity_amount: amount, + liquidity_amount_type: amount_type, } } 6 => { @@ -541,12 +543,12 @@ impl LendingInstruction { buf.extend_from_slice(&collateral_amount.to_le_bytes()); } Self::BorrowObligationLiquidity { - amount, - amount_type, + liquidity_amount, + liquidity_amount_type, } => { buf.push(5); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.extend_from_slice(&amount_type.to_u8().unwrap().to_le_bytes()); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } Self::RepayObligationLiquidity { liquidity_amount } => { buf.push(6); @@ -758,58 +760,46 @@ pub fn withdraw_reserve_liquidity( #[allow(clippy::too_many_arguments)] pub fn borrow_obligation_liquidity( program_id: Pubkey, - amount: u64, - amount_type: BorrowAmountType, - source_collateral_pubkey: Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + source_liquidity_pubkey: Pubkey, destination_liquidity_pubkey: Pubkey, - deposit_reserve_pubkey: Pubkey, - deposit_reserve_collateral_supply_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, - borrow_reserve_liquidity_supply_pubkey: Pubkey, borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - lending_market_authority_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_token_mint_pubkey: Pubkey, - obligation_token_output_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + lending_market_pubkey: Pubkey, dex_market_pubkey: Pubkey, dex_market_order_book_side_pubkey: Pubkey, memory_pubkey: Pubkey, - deposit_reserve_collateral_host_pubkey: Option, + host_fee_receiver_pubkey: Option, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); let mut accounts = vec![ - AccountMeta::new(source_collateral_pubkey, false), + AccountMeta::new(source_liquidity_pubkey, false), AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new_readonly(deposit_reserve_pubkey, false), - AccountMeta::new(deposit_reserve_collateral_supply_pubkey, false), AccountMeta::new(borrow_reserve_pubkey, false), - AccountMeta::new(borrow_reserve_liquidity_supply_pubkey, false), AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(dex_market_pubkey, false), AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; - if let Some(deposit_reserve_collateral_host_pubkey) = deposit_reserve_collateral_host_pubkey { - accounts.push(AccountMeta::new( - deposit_reserve_collateral_host_pubkey, - false, - )); + if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { + accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); } Instruction { program_id, accounts, data: LendingInstruction::BorrowObligationLiquidity { - amount, - amount_type, + liquidity_amount, + liquidity_amount_type, } .pack(), } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 413489dce49..e6a555346f0 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,12 +1,13 @@ //! Program state processor +use crate::state::TokenConverter; use crate::{ dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, - instruction::{init_lending_market, BorrowAmountType, LendingInstruction}, - math::{Decimal, TryAdd, TryDiv, TryMul, TrySub, WAD}, + instruction::{init_lending_market, AmountType, LendingInstruction}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, state::{ - LendingMarket, LiquidateResult, NewObligationCollateralParams, + BorrowResult, LendingMarket, LiquidateResult, NewObligationCollateralParams, NewObligationLiquidityParams, NewObligationParams, NewReserveParams, Obligation, ObligationCollateral, ObligationLiquidity, RepayResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, @@ -27,6 +28,7 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::Account; +use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( @@ -70,11 +72,16 @@ pub fn process_instruction( process_withdraw_reserve_liquidity(program_id, collateral_amount, accounts) } LendingInstruction::BorrowObligationLiquidity { - amount, - amount_type, + liquidity_amount, + liquidity_amount_type, } => { msg!("Instruction: Borrow"); - process_borrow_obligation_liquidity(program_id, amount, amount_type, accounts) + process_borrow_obligation_liquidity( + program_id, + liquidity_amount, + liquidity_amount_type, + accounts, + ) } LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { msg!("Instruction: Repay"); @@ -571,32 +578,32 @@ fn process_withdraw_reserve_liquidity( Ok(()) } -// @FIXME #[inline(never)] // avoid stack frame limit fn process_borrow_obligation_liquidity( program_id: &Pubkey, - token_amount: u64, - token_amount_type: BorrowAmountType, + liquidity_amount: u64, + liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { - if token_amount == 0 { + if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } let account_info_iter = &mut accounts.iter(); - let source_collateral_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; - let deposit_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; let dex_market_info = next_account_info(account_info_iter)?; let dex_market_orders_info = next_account_info(account_info_iter)?; let memory = next_account_info(account_info_iter)?; @@ -616,45 +623,17 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if borrow_reserve.lending_market != deposit_reserve.lending_market { + if &borrow_reserve.lending_market != lending_market_info.key { return Err(LendingError::LendingMarketMismatch.into()); } - - // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? - if deposit_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } - if deposit_reserve_info.key == borrow_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if deposit_reserve.liquidity.mint_pubkey == borrow_reserve.liquidity.mint_pubkey { - return Err(LendingError::DuplicateReserveMint.into()); - } - if &borrow_reserve.liquidity.supply_pubkey != borrow_reserve_liquidity_supply_info.key { + if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { msg!("Invalid borrow reserve liquidity supply account input"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey != deposit_reserve_collateral_supply_info.key { - msg!("Invalid deposit reserve collateral supply account input"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Cannot use deposit reserve collateral supply as source account input"); - return Err(LendingError::InvalidAccountInput.into()); - } if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { msg!("Cannot use borrow reserve liquidity supply as destination account input"); return Err(LendingError::InvalidAccountInput.into()); @@ -663,10 +642,8 @@ fn process_borrow_obligation_liquidity( msg!("Invalid borrow reserve liquidity fee receiver account"); return Err(LendingError::InvalidAccountInput.into()); } - - // TODO: handle case when neither reserve is the quote currency - if borrow_reserve.dex_market.is_none() && deposit_reserve.dex_market.is_none() { - msg!("One reserve must have a dex market"); + if borrow_reserve.dex_market.is_none() { + msg!("Borrow reserve must have a dex market"); return Err(LendingError::InvalidAccountInput.into()); } if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { @@ -675,42 +652,39 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } } - if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); - return Err(LendingError::InvalidAccountInput.into()); - } - } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.borrow_reserve != borrow_reserve_info.key { - msg!("Borrow reserve input doesn't match existing obligation borrow reserve"); + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation.collateral_reserve != deposit_reserve_info.key { - msg!("Collateral reserve input doesn't match existing obligation collateral reserve"); - return Err(LendingError::InvalidAccountInput.into()); + if obligation.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); } - - unpack_mint(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update_slot < borrow_reserve.last_update_slot { + return Err(LendingError::ObligationStale.into()); } - if &obligation.token_mint != obligation_token_mint_info.key { - msg!("Obligation token mint input doesn't match existing obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } - - let obligation_token_output = Account::unpack(&obligation_token_output_info.data.borrow())?; - if obligation_token_output_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); } - if &obligation_token_output.mint != obligation_token_mint_info.key { - return Err(LendingError::InvalidTokenMint.into()); + if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { + msg!("Invalid withdraw reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + return Err(LendingError::ObligationAccountNotFound.into()); } let authority_signer_seeds = &[ @@ -723,91 +697,91 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - assert_last_update_slot(&borrow_reserve, clock.slot)?; - assert_last_update_slot(&deposit_reserve, clock.slot)?; - - obligation.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); + let obligation_ltv = obligation.loan_to_value()?; + if obligation_ltv > lending_market_ltv { + return Err(LendingError::ObligationLTVAboveReserveLTV.into()); + } + if obligation_ltv == lending_market_ltv { + return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); + } let trade_simulator = TradeSimulator::new( dex_market_info, dex_market_orders_info, memory, &lending_market.quote_token_mint, + // @TODO: check these &borrow_reserve.liquidity.mint_pubkey, - &deposit_reserve.liquidity.mint_pubkey, + &lending_market.quote_token_mint, )?; - let loan = deposit_reserve.create_loan( - token_amount, - token_amount_type, + let max_borrow_value = obligation + .collateral_value + .try_mul(lending_market_ltv)? + .try_sub(obligation.liquidity_value)?; + + let BorrowResult { + total_amount, + borrow_amount, + origination_fee, + host_fee, + } = borrow_reserve.borrow( + liquidity_amount, + liquidity_amount_type, + max_borrow_value, trade_simulator, - &borrow_reserve.liquidity.mint_pubkey, + &lending_market.quote_token_mint, )?; - borrow_reserve.liquidity.borrow(loan.borrow_amount)?; - obligation.borrowed_liquidity_wads = obligation - .borrowed_liquidity_wads - .try_add(Decimal::from(loan.borrow_amount))?; - // @FIXME: unchecked math - obligation.deposited_collateral_tokens += loan.collateral_amount; + // @TODO: will this need adjustment for fees? + borrow_reserve + .liquidity + .borrow(total_amount, borrow_amount)?; + // @TODO: will this need adjustment for fees? + obligation_liquidity.borrow(borrow_amount); + obligation_liquidity.mark_stale(); + obligation.mark_stale(); + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - // deposit collateral - spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), - destination: deposit_reserve_collateral_supply_info.clone(), - amount: loan.collateral_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; + let mut owner_fee = origination_fee; + if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { + if host_fee > 0 { + owner_fee = owner_fee + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; - // transfer host fees if host is specified - let mut owner_fee = loan.origination_fee; - if let Ok(host_fee_recipient) = next_account_info(account_info_iter) { - if loan.host_fee > 0 { - // @FIXME: unchecked math - owner_fee -= loan.host_fee; spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), - destination: host_fee_recipient.clone(), - amount: loan.host_fee, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], + source: source_liquidity_info.clone(), + destination: host_fee_receiver_info.clone(), + amount: host_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, token_program: token_program_id.clone(), })?; } } - - // transfer remaining fees to owner if owner_fee > 0 { spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), + source: source_liquidity_info.clone(), destination: borrow_reserve_liquidity_fee_receiver_info.clone(), amount: owner_fee, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], + authority: lending_market_authority_info.clone(), + authority_signer_seeds, token_program: token_program_id.clone(), })?; } - // borrow liquidity spl_token_transfer(TokenTransferParams { - source: borrow_reserve_liquidity_supply_info.clone(), + source: source_liquidity_info.clone(), destination: destination_liquidity_info.clone(), - amount: loan.borrow_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - - // mint obligation tokens to output account - spl_token_mint_to(TokenMintToParams { - mint: obligation_token_mint_info.clone(), - destination: obligation_token_output_info.clone(), - amount: loan.collateral_amount, + amount: borrow_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 53431139540..61191483762 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -1,7 +1,7 @@ use super::*; use crate::{ error::LendingError, - instruction::BorrowAmountType, + instruction::AmountType, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; @@ -183,90 +183,59 @@ impl Reserve { } } - /// Create new loan - pub fn create_loan( + /// Borrow liquidity up to a maximum market value + pub fn borrow( &self, - token_amount: u64, - token_amount_type: BorrowAmountType, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + max_borrow_value: Decimal, token_converter: impl TokenConverter, - borrow_amount_token_mint: &Pubkey, - ) -> Result { - let (borrow_amount, mut collateral_amount) = match token_amount_type { - BorrowAmountType::CollateralDepositAmount => { - let collateral_amount = token_amount; - let borrow_amount = - self.allowed_borrow_for_collateral(collateral_amount, token_converter)?; - if borrow_amount == 0 { - return Err(LendingError::InvalidAmount.into()); + quote_token_mint: &Pubkey, + ) -> Result { + match liquidity_amount_type { + AmountType::ExactAmount => { + let borrow_amount = liquidity_amount; + let (origination_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(borrow_amount, false)?; + let total_amount = borrow_amount + .checked_add(origination_fee) + .ok_or(LendingError::MathOverflow)?; + let total_value = + token_converter.convert(total_amount.into(), &self.liquidity.mint_pubkey)?; + if total_value > max_borrow_value { + return Err(LendingError::BorrowTooLarge.into()); } - (borrow_amount, collateral_amount) + + Ok(BorrowResult { + total_amount, + borrow_amount, + origination_fee, + host_fee, + }) } - BorrowAmountType::LiquidityBorrowAmount => { - let borrow_amount = token_amount; - let collateral_amount = self.required_collateral_for_borrow( + AmountType::PercentAmount => { + let total_value = max_borrow_value + .try_mul(Decimal::from_percent(u8::try_from(liquidity_amount)?))?; + let total_amount = token_converter + .convert(total_value, "e_token_mint)? + .try_floor_u64()? + .min(self.liquidity.available_amount); + let (origination_fee, host_fee) = + self.config.fees.calculate_borrow_fees(total_amount, true)?; + let borrow_amount = total_amount + .checked_sub(origination_fee) + .ok_or(LendingError::MathOverflow)?; + + Ok(BorrowResult { + total_amount, borrow_amount, - borrow_amount_token_mint, - token_converter, - )?; - if collateral_amount == 0 { - return Err(LendingError::InvalidAmount.into()); - } - (borrow_amount, collateral_amount) + origination_fee, + host_fee, + }) } - }; - - let (origination_fee, host_fee) = self.config.fees.calculate_borrow_fees(borrow_amount)?; - - // @FIXME: fees - collateral_amount = collateral_amount - .checked_sub(origination_fee) - .ok_or(LendingError::MathOverflow)?; - - Ok(LoanResult { - borrow_amount, - collateral_amount, - origination_fee, - host_fee, - }) - } - - // @FIXME: move/remove this - /// Calculate allowed borrow for collateral - pub fn allowed_borrow_for_collateral( - &self, - collateral_amount: u64, - converter: impl TokenConverter, - ) -> Result { - let collateral_exchange_rate = self.collateral_exchange_rate()?; - let collateral_amount = Decimal::from(collateral_amount) - .try_mul(Rate::from_percent(self.config.loan_to_value_ratio))?; - let liquidity_amount = - collateral_exchange_rate.decimal_collateral_to_liquidity(collateral_amount)?; - - let borrow_amount = converter - .convert(liquidity_amount, &self.liquidity.mint_pubkey)? - .try_floor_u64()?; - - Ok(borrow_amount) - } - - // @FIXME: move/remove this - /// Calculate required collateral for borrow - pub fn required_collateral_for_borrow( - &self, - borrow_amount: u64, - borrow_amount_token_mint: &Pubkey, - converter: impl TokenConverter, - ) -> Result { - let collateral_exchange_rate = self.collateral_exchange_rate()?; - let liquidity_amount = - converter.convert(Decimal::from(borrow_amount), borrow_amount_token_mint)?; - let collateral_amount = collateral_exchange_rate - .decimal_liquidity_to_collateral(liquidity_amount)? - .try_div(Rate::from_percent(self.config.loan_to_value_ratio))? - .try_ceil_u64()?; - - Ok(collateral_amount) + } } /// Record deposited liquidity and return amount of collateral tokens to mint @@ -374,12 +343,12 @@ pub struct NewReserveParams { pub config: ReserveConfig, } -/// Create loan result -pub struct LoanResult { - /// Approved borrow amount +/// Create borrow result +pub struct BorrowResult { + /// Total amount of borrow plus origination fee + pub total_amount: u64, + /// Borrow amount portion of total amount pub borrow_amount: u64, - /// Required collateral amount - pub collateral_amount: u64, /// Loan origination fee pub origination_fee: u64, /// Host fee portion of origination fee @@ -439,15 +408,15 @@ impl ReserveLiquidity { Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads) } - /// Add new borrow amount to total borrows - pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { - if borrow_amount > self.available_amount { + /// Subtract total amount from available liquidity and add borrow amount to total borrows + pub fn borrow(&mut self, total_amount: u64, borrow_amount: u64) -> ProgramResult { + if total_amount > self.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } - self.available_amount -= self + self.available_amount = self .available_amount - .checked_sub(borrow_amount) + .checked_sub(total_amount) .ok_or(LendingError::MathOverflow)?; self.borrowed_amount_wads = self .borrowed_amount_wads @@ -455,7 +424,7 @@ impl ReserveLiquidity { Ok(()) } - /// Subtract repay amount from total borrows and add to available liquidity + /// 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 @@ -590,7 +559,11 @@ pub struct ReserveFees { impl ReserveFees { /// Calculate the owner and host fees on borrow - pub fn calculate_borrow_fees(&self, borrow_amount: u64) -> Result<(u64, u64), ProgramError> { + pub fn calculate_borrow_fees( + &self, + borrow_amount: u64, + inclusive: bool, + ) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); if borrow_fee_rate > Rate::zero() && borrow_amount > 0 { @@ -601,10 +574,16 @@ impl ReserveFees { 1 // 1 token to owner, nothing else }; - let borrow_fee = borrow_fee_rate - .try_mul(borrow_amount)? - .try_round_u64()? - .max(minimum_fee); + let borrow_fee_amount = if inclusive { + // inclusive fee = (rate / (1 + rate)) * amount + borrow_fee_rate + .try_div(borrow_fee_rate.try_add(Rate::one())?)? + .try_mul(borrow_amount)? + } else { + borrow_fee_rate.try_mul(borrow_amount)? + }; + + let borrow_fee = borrow_fee_amount.try_round_u64()?.max(minimum_fee); let host_fee = if need_to_assess_host_fee { host_fee_rate.try_mul(borrow_fee)?.try_round_u64()?.max(1) diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index 9824927c671..fce7ff57753 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -6,7 +6,7 @@ use helpers::*; use solana_program_test::*; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_lending::{ - instruction::BorrowAmountType, math::Decimal, processor::process_instruction, + instruction::AmountType, math::Decimal, processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, }; @@ -102,7 +102,7 @@ async fn test_borrow_quote_currency() { deposit_reserve: &sol_reserve, borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: collateral_deposit_amount, user_accounts_owner: &user_accounts_owner, obligation: &usdc_obligation, @@ -133,7 +133,7 @@ async fn test_borrow_quote_currency() { deposit_reserve: &sol_reserve, borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount, + borrow_amount_type: AmountType::ExactAmount, amount: borrow_amount, user_accounts_owner: &user_accounts_owner, obligation: &usdc_obligation, @@ -260,7 +260,7 @@ async fn test_borrow_base_currency() { deposit_reserve: &usdc_reserve, borrow_reserve: &sol_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: collateral_deposit_amount, user_accounts_owner: &user_accounts_owner, obligation: &sol_obligation, @@ -291,7 +291,7 @@ async fn test_borrow_base_currency() { deposit_reserve: &usdc_reserve, borrow_reserve: &sol_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount, + borrow_amount_type: AmountType::ExactAmount, amount: borrow_amount, user_accounts_owner: &user_accounts_owner, obligation: &sol_obligation, diff --git a/token-lending/program/tests/genesis_accounts.rs b/token-lending/program/tests/genesis_accounts.rs index 7c0f27c4a36..85502886195 100644 --- a/token-lending/program/tests/genesis_accounts.rs +++ b/token-lending/program/tests/genesis_accounts.rs @@ -5,7 +5,7 @@ mod helpers; use helpers::*; use solana_sdk::signature::Keypair; use spl_token_lending::{ - instruction::BorrowAmountType, + instruction::AmountType, math::Decimal, state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION}, }; @@ -201,7 +201,7 @@ async fn test_success() { deposit_reserve: &sol_reserve, borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, user_accounts_owner: &user_accounts_owner, obligation: &usdc_obligation, @@ -218,7 +218,7 @@ async fn test_success() { deposit_reserve: &sol_reserve, borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS / 100, @@ -253,7 +253,7 @@ async fn test_success() { deposit_reserve: &usdc_reserve, borrow_reserve: &sol_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: INITIAL_COLLATERAL_RATIO * lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 @@ -275,7 +275,7 @@ async fn test_success() { deposit_reserve: &usdc_reserve, borrow_reserve: &srm_reserve, dex_market: &srm_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + borrow_amount_type: AmountType::PercentAmount, amount: INITIAL_COLLATERAL_RATIO * lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 375ec57d6d9..6b59949edbf 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -16,7 +16,7 @@ use spl_token::{ use spl_token_lending::{ instruction::{ borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, - init_obligation, init_reserve, liquidate_obligation, BorrowAmountType, + init_obligation, init_reserve, liquidate_obligation, AmountType, }, math::{Decimal, Rate, TryAdd, TryMul}, processor::process_instruction, @@ -431,7 +431,7 @@ pub struct TestLendingMarket { pub struct BorrowArgs<'a> { pub deposit_reserve: &'a TestReserve, pub borrow_reserve: &'a TestReserve, - pub borrow_amount_type: BorrowAmountType, + pub borrow_amount_type: AmountType, pub amount: u64, pub dex_market: &'a TestDexMarket, pub user_accounts_owner: &'a Keypair, @@ -636,7 +636,7 @@ impl TestLendingMarket { dex_market.bids_pubkey }; - let approve_amount = if borrow_amount_type == BorrowAmountType::CollateralDepositAmount { + let approve_amount = if borrow_amount_type == AmountType::PercentAmount { amount } else { get_token_balance(banks_client, deposit_reserve.user_collateral_account).await From 9c902a663ee80b9819601d55643b8d355ae20069 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:29:05 -0500 Subject: [PATCH 054/191] add amount type to other instructions --- token-lending/program/src/instruction.rs | 45 +++++++++++++--- token-lending/program/src/processor.rs | 65 +++++++++++++++++++++--- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index f744d840330..99b088160a9 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -181,8 +181,10 @@ pub enum LendingInstruction { /// 8. `[]` Clock sysvar /// 9. `[]` Token program id RepayObligationLiquidity { - /// Amount of liquidity to repay + /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, }, // 7 @@ -211,8 +213,10 @@ pub enum LendingInstruction { /// 13 `[]` Clock sysvar /// 14 `[]` Token program id LiquidateObligation { - /// Amount of loan to repay + /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, }, // 8 @@ -269,8 +273,10 @@ pub enum LendingInstruction { /// 10 `[]` Clock sysvar /// 11 `[]` Token program id WithdrawObligationCollateral { - /// Amount of collateral to withdraw + /// Amount of collateral to withdraw - usage depends on `collateral_amount_type` collateral_amount: u64, + /// Describe how `collateral_amount` should be treated + collateral_amount_type: AmountType, }, // 11 @@ -550,9 +556,13 @@ impl LendingInstruction { buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RepayObligationLiquidity { liquidity_amount } => { + Self::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } => { buf.push(6); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } Self::LiquidateObligation { liquidity_amount } => { buf.push(7); @@ -565,9 +575,13 @@ impl LendingInstruction { buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::WithdrawObligationCollateral { collateral_amount } => { + Self::WithdrawObligationCollateral { + collateral_amount, + collateral_amount_type, + } => { buf.push(10); buf.extend_from_slice(&collateral_amount.to_le_bytes()); + buf.extend_from_slice(&collateral_amount_type.to_u8().unwrap().to_le_bytes()); } Self::SetLendingMarketOwner { new_owner } => { buf.push(11); @@ -811,6 +825,7 @@ pub fn borrow_obligation_liquidity( pub fn repay_obligation_liquidity( program_id: Pubkey, liquidity_amount: u64, + liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, @@ -843,7 +858,11 @@ pub fn repay_obligation_liquidity( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), + data: LendingInstruction::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } + .pack(), } } @@ -853,6 +872,7 @@ pub fn repay_obligation_liquidity( pub fn liquidate_obligation( program_id: Pubkey, liquidity_amount: u64, + liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, @@ -887,7 +907,11 @@ pub fn liquidate_obligation( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), + data: LendingInstruction::LiquidateObligation { + liquidity_amount, + liquidity_amount_type, + } + .pack(), } } @@ -948,6 +972,7 @@ pub fn deposit_obligation_collateral( pub fn withdraw_obligation_collateral( program_id: Pubkey, collateral_amount: u64, + collateral_amount_type: AmountType, source_collateral_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, withdraw_reserve_pubkey: Pubkey, @@ -976,7 +1001,11 @@ pub fn withdraw_obligation_collateral( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::WithdrawObligationCollateral { collateral_amount }.pack(), + data: LendingInstruction::WithdrawObligationCollateral { + collateral_amount, + collateral_amount_type, + } + .pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e6a555346f0..fbb1f17a3b0 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -83,13 +83,29 @@ pub fn process_instruction( accounts, ) } - LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { + LendingInstruction::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } => { msg!("Instruction: Repay"); - process_repay_obligation_liquidity(program_id, liquidity_amount, accounts) + process_repay_obligation_liquidity( + program_id, + liquidity_amount, + liquidity_amount_type, + accounts, + ) } - LendingInstruction::LiquidateObligation { liquidity_amount } => { + LendingInstruction::LiquidateObligation { + liquidity_amount, + liquidity_amount_type, + } => { msg!("Instruction: Liquidate"); - process_liquidate_obligation(program_id, liquidity_amount, accounts) + process_liquidate_obligation( + program_id, + liquidity_amount, + liquidity_amount_type, + accounts, + ) } LendingInstruction::AccrueReserveInterest => { msg!("Instruction: Accrue Interest"); @@ -99,9 +115,17 @@ pub fn process_instruction( msg!("Instruction: Deposit Obligation Collateral"); process_deposit_obligation_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::WithdrawObligationCollateral { collateral_amount } => { + LendingInstruction::WithdrawObligationCollateral { + collateral_amount, + collateral_amount_type, + } => { msg!("Instruction: Withdraw Obligation Collateral"); - process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) + process_withdraw_obligation_collateral( + program_id, + collateral_amount, + collateral_amount_type, + accounts, + ) } LendingInstruction::SetLendingMarketOwner { new_owner } => { msg!("Instruction: Set Lending Market Owner"); @@ -795,11 +819,19 @@ fn process_borrow_obligation_liquidity( fn process_repay_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, + // @FIXME + liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; @@ -905,6 +937,8 @@ fn process_repay_obligation_liquidity( obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; + // @FIXME: use liquidity_amount_type + let RepayResult { integer_repay_amount, decimal_repay_amount, @@ -956,11 +990,18 @@ fn process_repay_obligation_liquidity( fn process_liquidate_obligation( program_id: &Pubkey, liquidity_amount: u64, + liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; @@ -1091,6 +1132,8 @@ fn process_liquidate_obligation( &repay_reserve.liquidity.mint_pubkey, )?; + // @FIXME: use liquidity_amount_type + let LiquidateResult { withdraw_amount, repay_amount, @@ -1290,13 +1333,19 @@ fn process_deposit_obligation_collateral( #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral( program_id: &Pubkey, - // @TODO: allow fixed or max withdraw collateral_amount: u64, + collateral_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { return Err(LendingError::InvalidAmount.into()); } + if let AmountType::PercentAmount = collateral_amount_type { + if collateral_amount > 100 { + msg!("Collateral amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; @@ -1412,6 +1461,8 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } + // @FIXME: use collateral_amount_type + if obligation_collateral.deposited_tokens == 0 { return Err(LendingError::ObligationEmpty.into()); } From 77180263be6b8caa328cd6a0a3ab1fa315e838a9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 00:45:02 -0500 Subject: [PATCH 055/191] fmt [skip ci] --- token-lending/program/src/processor.rs | 13 ++++--------- .../program/tests/withdraw_obligation_collateral.rs | 4 +--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index fbb1f17a3b0..de005b894ef 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1479,9 +1479,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } - let required_collateral_value = obligation - .liquidity_value - .try_div(lending_market_ltv)?; + let required_collateral_value = obligation.liquidity_value.try_div(lending_market_ltv)?; let collateral_value_difference = obligation .collateral_value .try_sub(required_collateral_value)?; @@ -1897,8 +1895,7 @@ fn process_refresh_obligation_liquidity( )?; obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; - obligation_liquidity - .update_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; + obligation_liquidity.update_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; obligation_liquidity.update_slot(clock.slot)?; ObligationLiquidity::pack( obligation_liquidity, @@ -1955,8 +1952,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ObligationCollateralStale.into()); } - collateral_value = - collateral_value.try_add(obligation_collateral.value)?; + collateral_value = collateral_value.try_add(obligation_collateral.value)?; } let mut liquidity_value = Decimal::zero(); @@ -1980,8 +1976,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ObligationLiquidityStale.into()); } - liquidity_value = - liquidity_value.try_add(obligation_liquidity.value)?; + liquidity_value = liquidity_value.try_add(obligation_liquidity.value)?; } // @TODO: check this diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 258b39c918f..c82eacbc350 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -328,9 +328,7 @@ async fn test_withdraw_below_required() { .unwrap(), TransactionError::InstructionError( 3, - InstructionError::Custom( - LendingError::ObligationLTVCannotGoAboveReserveLTV as u32 - ) + InstructionError::Custom(LendingError::ObligationLTVCannotGoAboveReserveLTV as u32) ) ); } From 89d1011156f3eddead74fb56ce6002abebebb2e3 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 12:44:31 -0500 Subject: [PATCH 056/191] add fixme comments --- token-lending/program/src/state/reserve.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 61191483762..048608dfbfb 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -74,6 +74,7 @@ impl Reserve { if low_utilization || self.config.optimal_utilization_rate == 100 { let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?; let min_rate = Rate::from_percent(self.config.min_borrow_rate); + // @FIXME: unchecked math let rate_range = Rate::from_percent(self.config.optimal_borrow_rate - self.config.min_borrow_rate); @@ -81,10 +82,12 @@ impl Reserve { } else { let normalized_rate = utilization_rate .try_sub(optimal_utilization_rate)? + // @FIXME: unchecked math .try_div(Rate::from_percent( 100 - self.config.optimal_utilization_rate, ))?; let min_rate = Rate::from_percent(self.config.optimal_borrow_rate); + // @FIXME: unchecked math let rate_range = Rate::from_percent(self.config.max_borrow_rate - self.config.optimal_borrow_rate); @@ -421,6 +424,7 @@ impl ReserveLiquidity { self.borrowed_amount_wads = self .borrowed_amount_wads .try_add(Decimal::from(borrow_amount))?; + Ok(()) } From 0988f5b082e3663143b1e9bea7d5c16dd7690a52 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 15:05:01 -0500 Subject: [PATCH 057/191] redeem collateral nit --- token-lending/program/src/instruction.rs | 2 +- token-lending/program/src/processor.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 99b088160a9..a281948f8e2 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -109,7 +109,7 @@ pub enum LendingInstruction { }, // 4 - // @TODO: consider renaming to SwapReserveCollateralForLiquidity + // @TODO: consider renaming to SwapReserveCollateralForLiquidity or RedeemReserveCollateral /// Withdraw tokens from a reserve. The input is a collateral token representing ownership /// of the reserve liquidity pool. /// diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index de005b894ef..b0ad7d794be 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -578,7 +578,7 @@ fn process_withdraw_reserve_liquidity( assert_last_update_slot(&reserve, clock.slot)?; - let liquidity_withdraw_amount = reserve.redeem_collateral(collateral_amount)?; + let liquidity_amount = reserve.redeem_collateral(collateral_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { @@ -593,7 +593,7 @@ fn process_withdraw_reserve_liquidity( spl_token_transfer(TokenTransferParams { source: reserve_liquidity_supply_info.clone(), destination: destination_liquidity_info.clone(), - amount: liquidity_withdraw_amount, + amount: liquidity_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), From 520193519823cae0178eac4f02e615110ec53747 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 15:06:56 -0500 Subject: [PATCH 058/191] add todos, safety checks --- token-lending/program/src/processor.rs | 32 ++++++++++++++++++++-- token-lending/program/src/state/reserve.rs | 2 ++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b0ad7d794be..f7ff177f354 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -704,12 +704,22 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { - msg!("Invalid withdraw reserve account"); + msg!("Invalid borrow reserve account"); return Err(LendingError::InvalidAccountInput.into()); } if !obligation.liquidity.contains(obligation_liquidity_info.key) { return Err(LendingError::ObligationAccountNotFound.into()); } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update_slot < obligation_liquidity.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? + if obligation_liquidity.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -721,6 +731,12 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } + // @TODO: is this necessary? + assert_last_update_slot(&borrow_reserve, clock.slot)?; + + // @TODO: is this necessary? + obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv > lending_market_ltv { @@ -758,11 +774,11 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; - // @TODO: will this need adjustment for fees? + // @TODO: will this need further adjustment for fees? borrow_reserve .liquidity .borrow(total_amount, borrow_amount)?; - // @TODO: will this need adjustment for fees? + // @TODO: will this need further adjustment for fees? obligation_liquidity.borrow(borrow_amount); obligation_liquidity.mark_stale(); obligation.mark_stale(); @@ -1428,6 +1444,9 @@ fn process_withdraw_obligation_collateral( { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation.last_update_slot < obligation_collateral.last_update_slot { + return Err(LendingError::ObligationCollateralStale.into()); + } if obligation_collateral.is_stale(clock.slot)? { return Err(LendingError::ObligationCollateralStale.into()); } @@ -1437,6 +1456,13 @@ fn process_withdraw_obligation_collateral( if obligation.last_update_slot < obligation_collateral.last_update_slot { return Err(LendingError::ObligationStale.into()); } + // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? + if obligation_collateral.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + if obligation_collateral.deposited_tokens == 0 { + return Err(LendingError::ObligationEmpty.into()); + } let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint_info.owner != token_program_id.key { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 048608dfbfb..71fba421da5 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -264,6 +264,8 @@ impl Reserve { let collateral_exchange_rate = self.collateral_exchange_rate()?; let liquidity_amount = collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?; + // @FIXME: collateral represents a share of available liquidity, + // it should never be more than what's available if liquidity_amount > self.liquidity.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } From 8a5602477f1028923cbd389f27edc39e48c17ae7 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 15:07:48 -0500 Subject: [PATCH 059/191] repay implementation --- token-lending/program/src/processor.rs | 143 ++++++++---------- token-lending/program/src/state/obligation.rs | 46 ------ .../program/src/state/obligation_liquidity.rs | 8 +- 3 files changed, 64 insertions(+), 133 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f7ff177f354..ba363a8b775 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -830,12 +830,10 @@ fn process_borrow_obligation_liquidity( Ok(()) } -// @FIXME #[inline(never)] // avoid stack frame limit fn process_repay_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, - // @FIXME liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { @@ -851,14 +849,11 @@ fn process_repay_obligation_liquidity( let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; let repay_reserve_info = next_account_info(account_info_iter)?; let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let withdraw_reserve_info = next_account_info(account_info_iter)?; - let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_input_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -873,69 +868,65 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation.collateral_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); + if &repay_reserve.liquidity.supply_pubkey != source_liquidity_info.key { + msg!("Invalid repay reserve liquidity supply account input"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.deposited_collateral_tokens == 0 { - return Err(LendingError::ObligationEmpty.into()); - } - - let obligation_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint account"); + if &repay_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + msg!("Cannot use repay reserve liquidity supply as destination account input"); return Err(LendingError::InvalidAccountInput.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - - let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; - if withdraw_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); + if obligation.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); } - if withdraw_reserve.lending_market != repay_reserve.lending_market { - return Err(LendingError::LendingMarketMismatch.into()); + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update_slot < repay_reserve.last_update_slot { + return Err(LendingError::ObligationStale.into()); } - if repay_reserve_info.key == withdraw_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if repay_reserve.liquidity.mint_pubkey == withdraw_reserve.liquidity.mint_pubkey { - return Err(LendingError::DuplicateReserveMint.into()); + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Invalid repay reserve liquidity supply account"); + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Invalid withdraw reserve collateral supply account"); + if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { + msg!("Invalid repay reserve account"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Cannot use repay reserve liquidity supply as source account input"); - return Err(LendingError::InvalidAccountInput.into()); + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + return Err(LendingError::ObligationAccountNotFound.into()); } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Cannot use withdraw reserve collateral supply as destination account input"); - return Err(LendingError::InvalidAccountInput.into()); + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update_slot < obligation_liquidity.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? + if obligation_liquidity.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); } let authority_signer_seeds = &[ @@ -948,56 +939,42 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // accrue interest and update rates + // @TODO: is this necessary? assert_last_update_slot(&repay_reserve, clock.slot)?; - obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; + // @TODO: is this necessary? + obligation_liquidity.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; - // @FIXME: use liquidity_amount_type + let settle_amount = match liquidity_amount_type { + AmountType::ExactAmount => { + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + } + AmountType::PercentAmount => Decimal::from_percent(u8::try_from(liquidity_amount)?) + .try_mul(obligation_liquidity.borrowed_wads)?, + }; + let repay_amount = settle_amount.try_floor_u64()?; - let RepayResult { - integer_repay_amount, - decimal_repay_amount, - collateral_withdraw_amount, - obligation_token_amount, - } = obligation.repay(liquidity_amount, obligation_mint.supply)?; - repay_reserve - .liquidity - .repay(integer_repay_amount, decimal_repay_amount)?; + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + obligation_liquidity.repay(settle_amount); + obligation_liquidity.mark_stale(); + obligation.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - // burn obligation tokens - spl_token_burn(TokenBurnParams { - mint: obligation_token_mint_info.clone(), - source: obligation_token_input_info.clone(), - amount: obligation_token_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; - - // deposit repaid liquidity spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: repay_reserve_liquidity_supply_info.clone(), - amount: integer_repay_amount, + amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; - // withdraw collateral - spl_token_transfer(TokenTransferParams { - source: withdraw_reserve_collateral_supply_info.clone(), - destination: destination_collateral_info.clone(), - amount: collateral_withdraw_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - Ok(()) } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 6a73a25118f..7711ef876e4 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -80,40 +80,6 @@ impl Obligation { Ok(()) } - // @FIXME - /// Repay borrowed tokens - pub fn repay( - &mut self, - liquidity_amount: u64, - obligation_token_supply: u64, - ) -> Result { - let decimal_repay_amount = Decimal::from(liquidity_amount).min(self.borrowed_wads); - let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?; - if integer_repay_amount == 0 { - return Err(LendingError::ObligationEmpty.into()); - } - - let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_wads)?; - let collateral_withdraw_amount = { - let withdraw_amount: Decimal = repay_pct.try_mul(self.deposited_collateral_tokens)?; - withdraw_amount.try_floor_u64()? - }; - - let obligation_token_amount = self.collateral_to_obligation_token_amount( - collateral_withdraw_amount, - obligation_token_supply, - )?; - - self.liquidate(decimal_repay_amount, collateral_withdraw_amount)?; - - Ok(RepayResult { - decimal_repay_amount, - integer_repay_amount, - collateral_withdraw_amount, - obligation_token_amount, - }) - } - /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { self.liquidity_value.try_div(self.collateral_value) @@ -143,18 +109,6 @@ impl Obligation { } } -/// Obligation repay result -pub struct RepayResult { - /// Amount of collateral to withdraw - pub collateral_withdraw_amount: u64, - /// Amount of obligation tokens to burn - pub obligation_token_amount: u64, - /// Amount that will be repaid as precise decimal - pub decimal_repay_amount: Decimal, - /// Amount that will be repaid as u64 - pub integer_repay_amount: u64, -} - impl Sealed for Obligation {} impl IsInitialized for Obligation { fn is_initialized(&self) -> bool { diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 118b4bf6a94..240abd85a50 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -60,14 +60,14 @@ impl ObligationLiquidity { } /// Decrease borrowed liquidity - pub fn repay(&mut self, liquidity_amount: u64) -> ProgramResult { - self.borrowed_wads = self.borrowed_wads.try_sub(liquidity_amount.into())?; + pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult { + self.borrowed_wads = self.borrowed_wads.try_sub(settle_amount)?; Ok(()) } /// Increase borrowed liquidity - pub fn borrow(&mut self, liquidity_amount: u64) -> ProgramResult { - self.borrowed_wads = self.borrowed_wads.try_add(liquidity_amount.into())?; + pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { + self.borrowed_wads = self.borrowed_wads.try_add(borrow_amount.into())?; Ok(()) } From e10933d9581e9a6019b4ea0462d285cc4b565bd1 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 15:08:51 -0500 Subject: [PATCH 060/191] withdraw collateral handles percent --- token-lending/program/src/processor.rs | 55 +++++++++++++++----------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ba363a8b775..cbf52f7f6d7 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1464,15 +1464,6 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @FIXME: use collateral_amount_type - - if obligation_collateral.deposited_tokens == 0 { - return Err(LendingError::ObligationEmpty.into()); - } - if obligation_collateral.deposited_tokens < collateral_amount { - return Err(LendingError::InvalidObligationCollateral.into()); - } - let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv > lending_market_ltv { @@ -1482,23 +1473,39 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } - let required_collateral_value = obligation.liquidity_value.try_div(lending_market_ltv)?; - let collateral_value_difference = obligation - .collateral_value - .try_sub(required_collateral_value)?; - let collateral_amount_pct_of_obligation_collateral_amount = - Decimal::from(collateral_amount).try_div(obligation_collateral.deposited_tokens)?; - let collateral_amount_value = obligation - .collateral_value - .try_mul(collateral_amount_pct_of_obligation_collateral_amount)?; - if collateral_amount_value > collateral_value_difference { - return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); - } + let min_collateral_value = obligation.liquidity_value.try_div(lending_market_ltv)?; + let max_withdraw_value = obligation.collateral_value.try_sub(min_collateral_value)?; + + let withdraw_amount = match collateral_amount_type { + AmountType::ExactAmount => { + let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_tokens); + let withdraw_pct = + Decimal::from(withdraw_amount).try_div(obligation_collateral.deposited_tokens)?; + let withdraw_value = obligation.collateral_value.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); + } + + withdraw_amount + } + AmountType::PercentAmount => { + let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); + let withdraw_value = max_withdraw_value + .try_mul(withdraw_pct)? + .min(obligation_collateral.value); + let withdraw_amount = withdraw_value + .try_div(obligation_collateral.value)? + .try_mul(obligation_collateral.deposited_tokens)? + .try_floor_u64()?; + + withdraw_amount + } + }; let obligation_token_amount = obligation_collateral - .collateral_to_obligation_token_amount(collateral_amount, obligation_token_mint.supply)?; + .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; - obligation_collateral.withdraw(collateral_amount)?; + obligation_collateral.withdraw(withdraw_amount)?; obligation_collateral.mark_stale(); obligation.mark_stale(); @@ -1520,7 +1527,7 @@ fn process_withdraw_obligation_collateral( spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), - amount: collateral_amount, + amount: withdraw_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), From 2ade896ce1e838bda294d57b3dce1a500265f424 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 15:09:04 -0500 Subject: [PATCH 061/191] converter -> token_converter --- token-lending/program/src/state/obligation_collateral.rs | 4 ++-- token-lending/program/src/state/obligation_liquidity.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index a94be04089c..7f74fb97b0f 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -84,13 +84,13 @@ impl ObligationCollateral { pub fn update_value( &mut self, collateral_exchange_rate: CollateralExchangeRate, - converter: impl TokenConverter, + token_converter: impl TokenConverter, liquidity_token_mint: &Pubkey, ) -> ProgramResult { let liquidity_amount = collateral_exchange_rate .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.value = converter.convert(liquidity_amount, liquidity_token_mint)?; + self.value = token_converter.convert(liquidity_amount, liquidity_token_mint)?; Ok(()) } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 240abd85a50..411da332b8a 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -107,11 +107,11 @@ impl ObligationLiquidity { /// Update market value of liquidity pub fn update_value( &mut self, - converter: impl TokenConverter, + token_converter: impl TokenConverter, from_token_mint: &Pubkey, ) -> ProgramResult { // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.value = converter.convert(self.borrowed_wads, from_token_mint)?; + self.value = token_converter.convert(self.borrowed_wads, from_token_mint)?; Ok(()) } From bfc68b5bab131d255062b3fb864f1681599c09d5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:49:50 -0500 Subject: [PATCH 062/191] clean up msg text --- token-lending/program/src/processor.rs | 52 +++++++++++--------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index cbf52f7f6d7..e60fd3381c2 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -198,7 +198,6 @@ fn process_init_lending_market( Ok(()) } -// @FIXME fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, @@ -253,7 +252,7 @@ fn process_init_reserve( let token_program_id = next_account_info(account_info_iter)?; if reserve_liquidity_supply_info.key == source_liquidity_info.key { - msg!("Cannot use reserve liquidity supply as source account input"); + msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -468,11 +467,11 @@ fn process_deposit_reserve_liquidity( return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Cannot use reserve liquidity supply as source account input"); + msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Cannot use reserve collateral supply as destination account input"); + msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -558,11 +557,11 @@ fn process_withdraw_reserve_liquidity( return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Cannot use reserve liquidity supply as destination account input"); + msg!("Invalid destination liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Cannot use reserve collateral supply as source account input"); + msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -655,11 +654,11 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::LendingMarketMismatch.into()); } if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Invalid borrow reserve liquidity supply account input"); + msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Cannot use borrow reserve liquidity supply as destination account input"); + msg!("Invalid destination liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { @@ -672,7 +671,7 @@ fn process_borrow_obligation_liquidity( } if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); + msg!("Invalid dex market account"); return Err(LendingError::InvalidAccountInput.into()); } } @@ -876,12 +875,13 @@ fn process_repay_obligation_liquidity( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Invalid repay reserve liquidity supply account input"); + // @TODO: how is the currency/mint of the liquidity known? + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Cannot use repay reserve liquidity supply as destination account input"); + if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { + msg!("Invalid destination liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -1228,11 +1228,11 @@ fn process_deposit_obligation_collateral( return Err(LendingError::ReserveCollateralDisabled.into()); } if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { - msg!("Invalid deposit reserve collateral supply account input"); + msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Cannot use deposit reserve collateral supply as source collateral account input"); + msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -1259,7 +1259,7 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Obligation token mint input doesn't match existing obligation token mint"); + msg!("Invalid obligation token mint"); return Err(LendingError::InvalidTokenMint.into()); } if !obligation @@ -1371,13 +1371,11 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { - msg!("Invalid withdraw reserve collateral supply account input"); + msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!( - "Cannot use withdraw reserve collateral supply as destination collateral account input" - ); + msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -1412,7 +1410,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Obligation token mint input doesn't match existing obligation token mint"); + msg!("Invalid obligation token mint"); return Err(LendingError::InvalidTokenMint.into()); } if !obligation @@ -1421,17 +1419,11 @@ fn process_withdraw_obligation_collateral( { return Err(LendingError::ObligationAccountNotFound.into()); } - if obligation.last_update_slot < obligation_collateral.last_update_slot { - return Err(LendingError::ObligationCollateralStale.into()); - } - if obligation_collateral.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel if obligation.last_update_slot < obligation_collateral.last_update_slot { - return Err(LendingError::ObligationStale.into()); + return Err(LendingError::ObligationCollateralStale.into()); } // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? if obligation_collateral.is_stale(clock.slot)? { @@ -1770,7 +1762,7 @@ fn process_refresh_obligation_collateral( } if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); + msg!("Invalid dex market account"); return Err(LendingError::InvalidAccountInput.into()); } } @@ -1866,7 +1858,7 @@ fn process_refresh_obligation_liquidity( } if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account input"); + msg!("Invalid dex market account"); return Err(LendingError::InvalidAccountInput.into()); } } From 2fa1964c221c4c55db55b981452ac572bb295ed7 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:50:02 -0500 Subject: [PATCH 063/191] use assert_rent_exempt --- token-lending/program/src/processor.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e60fd3381c2..b6902992bc7 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -276,9 +276,7 @@ fn process_init_reserve( let dex_market = if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { let dex_market_info = next_account_info(account_info_iter)?; // TODO: check that market state is owned by real serum dex program - if !rent.is_exempt(dex_market_info.lamports(), dex_market_info.data_len()) { - return Err(LendingError::NotRentExempt.into()); - } + assert_rent_exempt(rent, dex_market_info)?; let dex_market_data = &dex_market_info.data.borrow(); let market_quote_mint = DexMarket::pubkey_at_offset(&dex_market_data, QUOTE_MINT_OFFSET); From b9f71c5d386378ac6fcbf258eb1f55168743ef6a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:50:07 -0500 Subject: [PATCH 064/191] sort keys --- token-lending/program/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b6902992bc7..6788ae6b78e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -321,8 +321,8 @@ fn process_init_reserve( let mut reserve = Reserve::new(NewReserveParams { current_slot: clock.slot, lending_market: *lending_market_info.key, - collateral: reserve_collateral_info, liquidity: reserve_liquidity_info, + collateral: reserve_collateral_info, dex_market, config, }); From c62119068c86287f4d8b5168e7e91c374ac5eb72 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:50:35 -0500 Subject: [PATCH 065/191] fixme comment --- token-lending/program/src/processor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6788ae6b78e..cfe96891cb6 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1495,6 +1495,7 @@ fn process_withdraw_obligation_collateral( let obligation_token_amount = obligation_collateral .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; + // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? obligation_collateral.withdraw(withdraw_amount)?; obligation_collateral.mark_stale(); obligation.mark_stale(); From 225b9ba1c9fea4ff2507c723a73d0b865373e311 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:51:31 -0500 Subject: [PATCH 066/191] don't use default() --- token-lending/program/src/state/reserve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 71fba421da5..f3ae1168085 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -467,8 +467,8 @@ impl ReserveCollateral { pub fn new(mint_pubkey: Pubkey, supply_pubkey: Pubkey) -> Self { Self { mint_pubkey, + mint_total_supply: 0, supply_pubkey, - ..Self::default() } } From 1feba6d490de772378b38da055f51dfb11acf43e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:47:40 -0500 Subject: [PATCH 067/191] repay fixes --- token-lending/program/src/instruction.rs | 17 ++++------------- token-lending/program/src/processor.rs | 6 ++++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index a281948f8e2..cb70624675d 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -819,7 +819,6 @@ pub fn borrow_obligation_liquidity( } } -// @FIXME /// Creates a `RepayObligationLiquidity` instruction #[allow(clippy::too_many_arguments)] pub fn repay_obligation_liquidity( @@ -827,14 +826,10 @@ pub fn repay_obligation_liquidity( liquidity_amount: u64, liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, - destination_collateral_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, - repay_reserve_liquidity_supply_pubkey: Pubkey, - withdraw_reserve_pubkey: Pubkey, - withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_mint_pubkey: Pubkey, - obligation_output_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { @@ -844,14 +839,10 @@ pub fn repay_obligation_liquidity( program_id, accounts: vec![ AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), AccountMeta::new(repay_reserve_pubkey, false), - AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new_readonly(withdraw_reserve_pubkey, false), - AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_mint_pubkey, false), - AccountMeta::new(obligation_output_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index cfe96891cb6..e33278c30f4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -947,8 +947,10 @@ fn process_repay_obligation_liquidity( AmountType::ExactAmount => { Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) } - AmountType::PercentAmount => Decimal::from_percent(u8::try_from(liquidity_amount)?) - .try_mul(obligation_liquidity.borrowed_wads)?, + AmountType::PercentAmount => { + let settle_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); + settle_pct.try_mul(obligation_liquidity.borrowed_wads)? + }, }; let repay_amount = settle_amount.try_floor_u64()?; From 5c3f681d58266e0e81e1d93abca3ddb9f8059243 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:48:46 -0500 Subject: [PATCH 068/191] liquidate wip --- token-lending/program/src/instruction.rs | 44 ++-- token-lending/program/src/processor.rs | 244 +++++++++++++----- token-lending/program/src/state/obligation.rs | 11 - .../program/src/state/obligation_liquidity.rs | 11 +- token-lending/program/src/state/reserve.rs | 110 ++++---- 5 files changed, 264 insertions(+), 156 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index cb70624675d..186f351d647 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -196,22 +196,21 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Source liquidity token account /// Minted by repay reserve liquidity mint. - /// $authority can transfer $collateral_amount. + /// $authority can transfer $liquidity_amount. /// 1. `[writable]` Destination collateral token account /// Minted by withdraw reserve collateral mint /// 2. `[writable]` Repay reserve account /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[]` Withdraw reserve account + /// 4. `[writable]` Withdraw reserve account /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account /// 6. `[writable]` Obligation account - /// 6. `[writable]` Obligation collateral account - /// 6. `[writable]` Obligation liquidity account - /// 7. `[]` Lending market account - /// 8. `[]` Derived lending market authority - /// 9. `[signer]` User transfer authority ($authority) - /// 12 `[]` Temporary memory - /// 13 `[]` Clock sysvar - /// 14 `[]` Token program id + /// 7. `[writable]` Obligation collateral account + /// 8. `[writable]` Obligation liquidity account + /// 9. `[]` Lending market account + /// 10 `[]` Derived lending market authority + /// 11 `[signer]` User transfer authority ($authority) + /// 12 `[]` Clock sysvar + /// 13 `[]` Token program id LiquidateObligation { /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -860,6 +859,23 @@ pub fn repay_obligation_liquidity( // @FIXME /// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] +/// 0. `[writable]` Source liquidity token account +/// Minted by repay reserve liquidity mint. +/// $authority can transfer $liquidity_amount. +/// 1. `[writable]` Destination collateral token account +/// Minted by withdraw reserve collateral mint +/// 2. `[writable]` Repay reserve account +/// 3. `[writable]` Repay reserve liquidity supply SPL Token account +/// 4. `[writable]` Withdraw reserve account +/// 5. `[writable]` Withdraw reserve collateral supply SPL Token account +/// 6. `[writable]` Obligation account +/// 7. `[writable]` Obligation collateral account +/// 8. `[writable]` Obligation liquidity account +/// 9. `[]` Lending market account +/// 10 `[]` Derived lending market authority +/// 11 `[signer]` User transfer authority ($authority) +/// 12 `[]` Clock sysvar +/// 13 `[]` Token program id pub fn liquidate_obligation( program_id: Pubkey, liquidity_amount: u64, @@ -873,9 +889,6 @@ pub fn liquidate_obligation( obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); @@ -886,15 +899,12 @@ pub fn liquidate_obligation( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new_readonly(withdraw_reserve_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e33278c30f4..b2acbfa288e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -978,7 +978,6 @@ fn process_repay_obligation_liquidity( Ok(()) } -// @FIXME #[inline(never)] // avoid stack frame limit fn process_liquidate_obligation( program_id: &Pubkey, @@ -1004,12 +1003,11 @@ fn process_liquidate_obligation( let withdraw_reserve_info = next_account_info(account_info_iter)?; let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1026,22 +1024,6 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidTokenProgram.into()); } - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation.collateral_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation.deposited_collateral_tokens == 0 { - return Err(LendingError::ObligationEmpty.into()); - } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; if repay_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1050,6 +1032,14 @@ fn process_liquidate_obligation( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { + msg!("Invalid repay reserve liquidity supply account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Invalid source liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { @@ -1058,46 +1048,112 @@ fn process_liquidate_obligation( if withdraw_reserve.lending_market != repay_reserve.lending_market { return Err(LendingError::LendingMarketMismatch.into()); } + if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { + msg!("Invalid withdraw reserve collateral supply account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!("Invalid destination collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } + // @TODO: are these two checks necessary? + // what's the problem with repaying and receiving the same currency? if repay_reserve_info.key == withdraw_reserve_info.key { return Err(LendingError::DuplicateReserve.into()); } if repay_reserve.liquidity.mint_pubkey == withdraw_reserve.liquidity.mint_pubkey { return Err(LendingError::DuplicateReserveMint.into()); } - if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Invalid repay reserve liquidity supply account"); - return Err(LendingError::InvalidAccountInput.into()); + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } - if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Invalid withdraw reserve collateral supply account"); + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Cannot use repay reserve liquidity supply as source account input"); + if obligation.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update_slot < repay_reserve.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + if obligation.last_update_slot < withdraw_reserve.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Cannot use withdraw reserve collateral supply as destination account input"); + if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { + msg!("Invalid withdraw reserve account"); return Err(LendingError::InvalidAccountInput.into()); } + if &obligation_collateral.token_mint != obligation_token_mint_info.key { + msg!("Invalid obligation token mint"); + return Err(LendingError::InvalidTokenMint.into()); + } + if !obligation + .collateral + .contains(obligation_collateral_info.key) + { + return Err(LendingError::ObligationAccountNotFound.into()); + } + if obligation.last_update_slot < obligation_collateral.last_update_slot { + return Err(LendingError::ObligationCollateralStale.into()); + } + if obligation_collateral.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update_slot < obligation_collateral.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? + if obligation_collateral.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + if obligation_collateral.deposited_tokens == 0 { + return Err(LendingError::ObligationEmpty.into()); + } - // TODO: handle case when neither reserve is the quote currency - if repay_reserve.dex_market.is_none() && withdraw_reserve.dex_market.is_none() { - msg!("One reserve must have a dex market"); + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if let COption::Some(dex_market_pubkey) = repay_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } + if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { + msg!("Invalid repay reserve account"); + return Err(LendingError::InvalidAccountInput.into()); } - if let COption::Some(dex_market_pubkey) = withdraw_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + return Err(LendingError::ObligationAccountNotFound.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update_slot < obligation_liquidity.last_update_slot { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? + if obligation_liquidity.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); } let authority_signer_seeds = &[ @@ -1110,41 +1166,96 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - // accrue interest and update rates + // @TODO: is this necessary? assert_last_update_slot(&repay_reserve, clock.slot)?; assert_last_update_slot(&withdraw_reserve, clock.slot)?; - obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; + // @TODO: is this necessary? + obligation_liquidity.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; - let trade_simulator = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - &withdraw_reserve.liquidity.mint_pubkey, - &repay_reserve.liquidity.mint_pubkey, - )?; + let liquidation_threshold = + Rate::from_percent(lending_market.liquidation_threshold); + let obligation_ltv = obligation.loan_to_value()?; + if obligation_ltv < liquidation_threshold { + return Err(LendingError::HealthyObligation.into()); + } - // @FIXME: use liquidity_amount_type + // @FIXME: resume here - let LiquidateResult { - withdraw_amount, - repay_amount, - settle_amount, - } = withdraw_reserve.liquidate_obligation( - &obligation, - liquidity_amount, - &repay_reserve.liquidity.mint_pubkey, - trade_simulator, - )?; + let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); + + let mut settle_amount = match liquidity_amount_type { + AmountType::ExactAmount => { + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + } + AmountType::PercentAmount => { + let settle_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); + settle_pct.try_mul(obligation_liquidity.borrowed_wads)? + }, + }; + let mut repay_amount = settle_amount.try_floor_u64()?; + let mut withdraw_amount; + + let max_closeable_amount = obligation_liquidity.max_closeable_amount()?; + let close_amount = settle_amount.min(max_closeable_amount); + if close_amount > 0 { + settle_amount = obligation_liquidity.borrowed_wads; + repay_amount = close_amount.try_floor_u64()?; + withdraw_amount = obligation_collateral.deposited_tokens; + } + else { + // Calculate the amount of liquidity that will be repaid + let max_liquidation_amount = obligation_liquidity.max_liquidation_amount()?; + + settle_amount = settle_amount.min(max_liquidation_amount); + repay_amount = settle_amount.try_floor_u64()?; + + withdraw_amount = { + let receive_liquidity_amount = + token_converter.convert(settle_amount, liquidity_token_mint)?; + + let collateral_amount = withdraw_reserve.collateral_exchange_rate()? + .decimal_liquidity_to_collateral(receive_liquidity_amount)?; + + let bonus_rate = Rate::from_percent(withdraw_reserve.config.liquidation_bonus); + + let bonus_amount = collateral_amount.try_mul(bonus_rate)?; + + let withdraw_amount = collateral_amount.try_add(bonus_amount)? + .min(obligation_collateral.deposited_tokens.into()); + + if repay_amount == max_liquidation_amount { + withdraw_amount.try_ceil_u64()? + } else { + withdraw_amount.try_floor_u64()? + } + }; + + // TODO: charge less liquidity if withdraw value exceeds loan collateral + if withdraw_amount == obligation_collateral.deposited_tokens { + settle_amount = obligation_liquidity.borrowed_wads; + } + } + + // + + if withdraw_amount == 0 { + return Err(LendingError::LiquidationTooSmall.into()); + } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + obligation_liquidity.repay(settle_amount); + // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? + obligation_collateral.withdraw(withdraw_amount); + obligation_liquidity.mark_stale(); + obligation_collateral.mark_stale(); + obligation.mark_stale(); - obligation.liquidate(settle_amount, withdraw_amount)?; + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + ObligationCollateral::pack(obligation_collateral, &mut obligation_collateral_info.data.borrow_mut())?; + ObligationLiquidity::pack(obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut())?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - // deposit repaid liquidity spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: repay_reserve_liquidity_supply_info.clone(), @@ -1154,7 +1265,6 @@ fn process_liquidate_obligation( token_program: token_program_id.clone(), })?; - // withdraw collateral spl_token_transfer(TokenTransferParams { source: withdraw_reserve_collateral_supply_info.clone(), destination: destination_collateral_info.clone(), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 7711ef876e4..093262fd385 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -69,17 +69,6 @@ impl Obligation { } } - // @FIXME - /// Liquidate part of obligation - pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { - self.borrowed_wads = self.borrowed_wads.try_sub(repay_amount)?; - self.deposited_collateral_tokens = self - .deposited_collateral_tokens - .checked_sub(withdraw_amount) - .ok_or(LendingError::MathOverflow)?; - Ok(()) - } - /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { self.liquidity_value.try_div(self.collateral_value) diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 411da332b8a..9d261638357 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -73,19 +73,18 @@ impl ObligationLiquidity { /// Maximum amount of loan that can be closed out by a liquidator due to the remaining balance /// being too small to be liquidated normally. - pub fn max_closeable_amount(&self) -> Result { + pub fn max_closeable_amount(&self) -> Result { if self.borrowed_wads < Decimal::from(CLOSEABLE_AMOUNT) { - self.borrowed_wads.try_ceil_u64() + Ok(self.borrowed_wads) } else { - Ok(0) + Ok(Decimal::zero()) } } /// Maximum amount of loan that can be repaid by liquidators - pub fn max_liquidation_amount(&self) -> Result { + pub fn max_liquidation_amount(&self) -> Result { self.borrowed_wads - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .try_floor_u64() + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR)) } /// Accrue interest diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f3ae1168085..8ae3a992e12 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -95,6 +95,61 @@ impl Reserve { } } + /// Borrow liquidity up to a maximum market value + pub fn borrow( + &self, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + max_borrow_value: Decimal, + token_converter: impl TokenConverter, + quote_token_mint: &Pubkey, + ) -> Result { + match liquidity_amount_type { + AmountType::ExactAmount => { + let borrow_amount = liquidity_amount; + let (origination_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(borrow_amount, false)?; + let total_amount = borrow_amount + .checked_add(origination_fee) + .ok_or(LendingError::MathOverflow)?; + let total_value = + token_converter.convert(total_amount.into(), &self.liquidity.mint_pubkey)?; + if total_value > max_borrow_value { + return Err(LendingError::BorrowTooLarge.into()); + } + + Ok(BorrowResult { + total_amount, + borrow_amount, + origination_fee, + host_fee, + }) + } + AmountType::PercentAmount => { + let total_value = max_borrow_value + .try_mul(Decimal::from_percent(u8::try_from(liquidity_amount)?))?; + let total_amount = token_converter + .convert(total_value, "e_token_mint)? + .try_floor_u64()? + .min(self.liquidity.available_amount); + let (origination_fee, host_fee) = + self.config.fees.calculate_borrow_fees(total_amount, true)?; + let borrow_amount = total_amount + .checked_sub(origination_fee) + .ok_or(LendingError::MathOverflow)?; + + Ok(BorrowResult { + total_amount, + borrow_amount, + origination_fee, + host_fee, + }) + } + } + } + // @FIXME /// Liquidate part of an unhealthy obligation pub fn liquidate_obligation( @@ -186,61 +241,6 @@ impl Reserve { } } - /// Borrow liquidity up to a maximum market value - pub fn borrow( - &self, - liquidity_amount: u64, - liquidity_amount_type: AmountType, - max_borrow_value: Decimal, - token_converter: impl TokenConverter, - quote_token_mint: &Pubkey, - ) -> Result { - match liquidity_amount_type { - AmountType::ExactAmount => { - let borrow_amount = liquidity_amount; - let (origination_fee, host_fee) = self - .config - .fees - .calculate_borrow_fees(borrow_amount, false)?; - let total_amount = borrow_amount - .checked_add(origination_fee) - .ok_or(LendingError::MathOverflow)?; - let total_value = - token_converter.convert(total_amount.into(), &self.liquidity.mint_pubkey)?; - if total_value > max_borrow_value { - return Err(LendingError::BorrowTooLarge.into()); - } - - Ok(BorrowResult { - total_amount, - borrow_amount, - origination_fee, - host_fee, - }) - } - AmountType::PercentAmount => { - let total_value = max_borrow_value - .try_mul(Decimal::from_percent(u8::try_from(liquidity_amount)?))?; - let total_amount = token_converter - .convert(total_value, "e_token_mint)? - .try_floor_u64()? - .min(self.liquidity.available_amount); - let (origination_fee, host_fee) = - self.config.fees.calculate_borrow_fees(total_amount, true)?; - let borrow_amount = total_amount - .checked_sub(origination_fee) - .ok_or(LendingError::MathOverflow)?; - - Ok(BorrowResult { - total_amount, - borrow_amount, - origination_fee, - host_fee, - }) - } - } - } - /// 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 From cf87372431f2f18ae0c4bab0f9c0bb680507f731 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 20:54:09 -0500 Subject: [PATCH 069/191] remove mem comments [skip ci] --- token-lending/program/src/processor.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b2acbfa288e..637ad92a923 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -631,7 +631,6 @@ fn process_borrow_obligation_liquidity( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - // Ensure memory is owned by this program so that we don't have to zero it out if memory.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } @@ -1011,7 +1010,6 @@ fn process_liquidate_obligation( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - // Ensure memory is owned by this program so that we don't have to zero it out if memory.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } @@ -1846,7 +1844,6 @@ fn process_refresh_obligation_collateral( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - // Ensure memory is owned by this program so that we don't have to zero it out if memory.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } @@ -1942,7 +1939,6 @@ fn process_refresh_obligation_liquidity( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - // Ensure memory is owned by this program so that we don't have to zero it out if memory.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } From 0ed483031ebcb479fee65aeec65be4a581afcfe2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 16 Mar 2021 21:24:06 -0500 Subject: [PATCH 070/191] fixme comment [skip ci] --- token-lending/program/src/state/reserve.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 8ae3a992e12..28ee208d18f 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -568,6 +568,7 @@ impl ReserveFees { pub fn calculate_borrow_fees( &self, borrow_amount: u64, + // @FIXME: use enum inclusive: bool, ) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); From 66e24f8aa31849f50e744d47f1e21bef678447b0 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:00:12 -0500 Subject: [PATCH 071/191] liquidate implementation --- token-lending/program/src/instruction.rs | 26 +-- token-lending/program/src/processor.rs | 148 ++++++----------- .../program/src/state/obligation_liquidity.rs | 16 -- token-lending/program/src/state/reserve.rs | 155 +++++++++--------- 4 files changed, 131 insertions(+), 214 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 186f351d647..6d45f2709f9 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -204,8 +204,8 @@ pub enum LendingInstruction { /// 4. `[writable]` Withdraw reserve account /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account /// 6. `[writable]` Obligation account - /// 7. `[writable]` Obligation collateral account - /// 8. `[writable]` Obligation liquidity account + /// 7. `[writable]` Obligation liquidity account + /// 8. `[writable]` Obligation collateral account /// 9. `[]` Lending market account /// 10 `[]` Derived lending market authority /// 11 `[signer]` User transfer authority ($authority) @@ -856,26 +856,8 @@ pub fn repay_obligation_liquidity( } } -// @FIXME /// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] -/// 0. `[writable]` Source liquidity token account -/// Minted by repay reserve liquidity mint. -/// $authority can transfer $liquidity_amount. -/// 1. `[writable]` Destination collateral token account -/// Minted by withdraw reserve collateral mint -/// 2. `[writable]` Repay reserve account -/// 3. `[writable]` Repay reserve liquidity supply SPL Token account -/// 4. `[writable]` Withdraw reserve account -/// 5. `[writable]` Withdraw reserve collateral supply SPL Token account -/// 6. `[writable]` Obligation account -/// 7. `[writable]` Obligation collateral account -/// 8. `[writable]` Obligation liquidity account -/// 9. `[]` Lending market account -/// 10 `[]` Derived lending market authority -/// 11 `[signer]` User transfer authority ($authority) -/// 12 `[]` Clock sysvar -/// 13 `[]` Token program id pub fn liquidate_obligation( program_id: Pubkey, liquidity_amount: u64, @@ -887,6 +869,8 @@ pub fn liquidate_obligation( withdraw_reserve_pubkey: Pubkey, withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { @@ -902,6 +886,8 @@ pub fn liquidate_obligation( AccountMeta::new(withdraw_reserve_pubkey, false), AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 637ad92a923..276c81fb09d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1002,8 +1002,8 @@ fn process_liquidate_obligation( let withdraw_reserve_info = next_account_info(account_info_iter)?; let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; let obligation_liquidity_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -1072,86 +1072,85 @@ fn process_liquidate_obligation( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.is_stale(clock.slot)? { + if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } // @TODO: is this enough? other reserves could have been updated that we don't check here, and // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update_slot < repay_reserve.last_update_slot { + if obligation.last_update.slot < repay_reserve.last_update.slot { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update_slot < withdraw_reserve.last_update_slot { + if obligation.last_update.slot < withdraw_reserve.last_update.slot { return Err(LendingError::ObligationStale.into()); } - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation_collateral.obligation != obligation_info.key { + if &obligation_liquidity.obligation != obligation_info.key { msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); + if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { + msg!("Invalid repay reserve account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); - } - if !obligation - .collateral - .contains(obligation_collateral_info.key) - { + if !obligation.liquidity.contains(obligation_liquidity_info.key) { return Err(LendingError::ObligationAccountNotFound.into()); } - if obligation.last_update_slot < obligation_collateral.last_update_slot { - return Err(LendingError::ObligationCollateralStale.into()); - } - if obligation_collateral.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update_slot < obligation_collateral.last_update_slot { + if obligation.last_update.slot < obligation_liquidity.last_update.slot { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? - if obligation_collateral.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); + // @TODO: is this necessary if checking obligation.last_update.slot < obligation_liquidity.last_update.slot above? + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); } - if obligation_collateral.deposited_tokens == 0 { + if obligation_liquidity.borrowed_wads == 0 { + // @TODO: make specific to liquidity return Err(LendingError::ObligationEmpty.into()); } - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation_liquidity.obligation != obligation_info.key { + if &obligation_collateral.obligation != obligation_info.key { msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); + if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { + msg!("Invalid withdraw reserve account"); return Err(LendingError::InvalidAccountInput.into()); } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { + if &obligation_collateral.token_mint != obligation_token_mint_info.key { + msg!("Invalid obligation token mint"); + return Err(LendingError::InvalidTokenMint.into()); + } + if !obligation + .collateral + .contains(obligation_collateral_info.key) + { return Err(LendingError::ObligationAccountNotFound.into()); } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update_slot < obligation_liquidity.last_update_slot { + if obligation.last_update.slot < obligation_collateral.last_update.slot { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? - if obligation_liquidity.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); + // @TODO: is this necessary if checking obligation.last_update.slot < obligation_liquidity.last_update.slot above? + if obligation_collateral.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + if obligation_collateral.deposited_tokens == 0 { + // @TODO: make specific to collateral + return Err(LendingError::ObligationEmpty.into()); } let authority_signer_seeds = &[ @@ -1178,64 +1177,17 @@ fn process_liquidate_obligation( return Err(LendingError::HealthyObligation.into()); } - // @FIXME: resume here - - let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); - - let mut settle_amount = match liquidity_amount_type { - AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) - } - AmountType::PercentAmount => { - let settle_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); - settle_pct.try_mul(obligation_liquidity.borrowed_wads)? - }, - }; - let mut repay_amount = settle_amount.try_floor_u64()?; - let mut withdraw_amount; - - let max_closeable_amount = obligation_liquidity.max_closeable_amount()?; - let close_amount = settle_amount.min(max_closeable_amount); - if close_amount > 0 { - settle_amount = obligation_liquidity.borrowed_wads; - repay_amount = close_amount.try_floor_u64()?; - withdraw_amount = obligation_collateral.deposited_tokens; - } - else { - // Calculate the amount of liquidity that will be repaid - let max_liquidation_amount = obligation_liquidity.max_liquidation_amount()?; - - settle_amount = settle_amount.min(max_liquidation_amount); - repay_amount = settle_amount.try_floor_u64()?; - - withdraw_amount = { - let receive_liquidity_amount = - token_converter.convert(settle_amount, liquidity_token_mint)?; - - let collateral_amount = withdraw_reserve.collateral_exchange_rate()? - .decimal_liquidity_to_collateral(receive_liquidity_amount)?; - - let bonus_rate = Rate::from_percent(withdraw_reserve.config.liquidation_bonus); - - let bonus_amount = collateral_amount.try_mul(bonus_rate)?; - - let withdraw_amount = collateral_amount.try_add(bonus_amount)? - .min(obligation_collateral.deposited_tokens.into()); - - if repay_amount == max_liquidation_amount { - withdraw_amount.try_ceil_u64()? - } else { - withdraw_amount.try_floor_u64()? - } - }; - - // TODO: charge less liquidity if withdraw value exceeds loan collateral - if withdraw_amount == obligation_collateral.deposited_tokens { - settle_amount = obligation_liquidity.borrowed_wads; - } - } - - // + let LiquidateResult { + settle_amount, + repay_amount, + withdraw_amount + } = withdraw_reserve.liquidate_obligation( + liquidity_amount, + liquidity_amount_type, + &obligation, + &obligation_liquidity, + &obligation_collateral, + )?; if withdraw_amount == 0 { return Err(LendingError::LiquidationTooSmall.into()); diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 9d261638357..1596fc8eb75 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -71,22 +71,6 @@ impl ObligationLiquidity { Ok(()) } - /// Maximum amount of loan that can be closed out by a liquidator due to the remaining balance - /// being too small to be liquidated normally. - pub fn max_closeable_amount(&self) -> Result { - if self.borrowed_wads < Decimal::from(CLOSEABLE_AMOUNT) { - Ok(self.borrowed_wads) - } else { - Ok(Decimal::zero()) - } - } - - /// Maximum amount of loan that can be repaid by liquidators - pub fn max_liquidation_amount(&self) -> Result { - self.borrowed_wads - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR)) - } - /// Accrue interest pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 28ee208d18f..822f376d385 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -18,8 +18,8 @@ use std::convert::{TryFrom, TryInto}; /// Percentage of an obligation that can be repaid during each liquidation call pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50; -/// Loan amount that is small enough to close out -pub const CLOSEABLE_AMOUNT: u64 = 2; +/// Obligation amount that is small enough to close out +pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2; /// Lending market reserve state #[derive(Clone, Debug, Default, PartialEq)] @@ -150,95 +150,90 @@ impl Reserve { } } - // @FIXME - /// Liquidate part of an unhealthy obligation + /// Liquidate some or all of an unhealthy obligation pub fn liquidate_obligation( &self, + liquidity_amount: u64, + liquidity_amount_type: AmountType, obligation: &Obligation, - liquidate_amount: u64, - liquidity_token_mint: &Pubkey, - token_converter: impl TokenConverter, + obligation_liquidity: &ObligationLiquidity, + obligation_collateral: &ObligationCollateral, ) -> Result { - Self::_liquidate_obligation( - obligation, - liquidate_amount, - liquidity_token_mint, - self.collateral_exchange_rate()?, - &self.config, - token_converter, - ) - } + let max_withdraw_amount = Decimal::from(obligation_collateral.deposited_tokens); + let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; - // @FIXME - fn _liquidate_obligation( - obligation: &Obligation, - liquidity_amount: u64, - liquidity_token_mint: &Pubkey, - collateral_exchange_rate: CollateralExchangeRate, - collateral_reserve_config: &ReserveConfig, - mut token_converter: impl TokenConverter, - ) -> Result { - // Check obligation health - let borrow_token_price = token_converter.best_price(liquidity_token_mint)?; - // @FIXME: moved to lending market - let liquidation_threshold = - Rate::from_percent(collateral_reserve_config.liquidation_threshold); - let obligation_loan_to_value = - obligation.loan_to_value(collateral_exchange_rate, borrow_token_price)?; - if obligation_loan_to_value < liquidation_threshold.into() { - return Err(LendingError::HealthyObligation.into()); - } - - // Special handling for small, closeable obligations - let max_closeable_amount = obligation.max_closeable_amount()?; - let close_amount = liquidity_amount.min(max_closeable_amount); - if close_amount > 0 { - return Ok(LiquidateResult { - withdraw_amount: obligation.deposited_collateral_tokens, - repay_amount: close_amount, - settle_amount: obligation.borrowed_liquidity_wads, - }); - } - - // Calculate the amount of liquidity that will be repaid - let max_liquidation_amount = obligation.max_liquidation_amount()?; - let repay_amount = liquidity_amount.min(max_liquidation_amount); - let decimal_repay_amount = Decimal::from(repay_amount); - - // Calculate the amount of collateral that will be received - let withdraw_amount = { - let receive_liquidity_amount = - token_converter.convert(decimal_repay_amount, liquidity_token_mint)?; - let collateral_amount = collateral_exchange_rate - .decimal_liquidity_to_collateral(receive_liquidity_amount)?; - let bonus_rate = Rate::from_percent(collateral_reserve_config.liquidation_bonus); - let bonus_amount = collateral_amount.try_mul(bonus_rate)?; - let withdraw_amount = collateral_amount.try_add(bonus_amount)?; - let withdraw_amount = - withdraw_amount.min(obligation.deposited_collateral_tokens.into()); - if repay_amount == max_liquidation_amount { - withdraw_amount.try_ceil_u64()? - } else { - withdraw_amount.try_floor_u64()? + let liquidate_amount = match liquidity_amount_type { + AmountType::ExactAmount => { + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + } + AmountType::PercentAmount => { + let liquidate_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); + obligation_liquidity.borrowed_wads.try_mul(liquidate_pct)? } }; - if withdraw_amount > 0 { - // TODO: charge less liquidity if withdraw value exceeds loan collateral - let settle_amount = if withdraw_amount == obligation.deposited_collateral_tokens { - obligation.borrowed_liquidity_wads + // Close out obligations that are too small to liquidate normally + if obligation_liquidity.borrowed_wads < LIQUIDATION_CLOSE_AMOUNT.into() { + let settle_amount = obligation_liquidity.borrowed_wads; + let settle_value = obligation_liquidity + .value + .try_mul(bonus_rate)? + .min(obligation_collateral.value); + let settle_pct = settle_value.try_div(obligation_collateral.value)?; + let repay_amount = liquidate_amount.min(settle_amount).try_ceil_u64()?; + + let collateral_amount = max_withdraw_amount.try_mul(settle_pct)?; + let withdraw_amount = if collateral_amount == max_withdraw_amount { + collateral_amount.try_ceil_u64()? } else { - decimal_repay_amount + collateral_amount.try_floor_u64()? }; - Ok(LiquidateResult { - withdraw_amount, - repay_amount, + return Ok(LiquidateResult { settle_amount, - }) - } else { - Err(LendingError::LiquidationTooSmall.into()) + repay_amount, + withdraw_amount, + }); } + + let max_liquidation_value = obligation + .liquidity_value + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? + .min(obligation_liquidity.value); + let max_liquidation_pct = max_liquidation_value.try_div(obligation_liquidity.value)?; + let max_liquidation_amount = obligation_liquidity + .borrowed_wads + .try_mul(max_liquidation_pct)?; + + let liquidation_amount = liquidate_amount.min(max_liquidation_amount); + let liquidation_pct = liquidation_amount.try_div(obligation_liquidity.borrowed_wads)?; + + let settle_value = obligation_liquidity + .value + .try_mul(liquidation_pct)? + .try_mul(bonus_rate)? + .min(obligation_collateral.value); + let settle_pct = settle_value.try_div(obligation_collateral.value)?; + let settle_amount = liquidation_amount.try_mul(settle_pct)?; + + let repay_amount = if settle_amount == max_liquidation_amount { + settle_amount.try_ceil_u64()? + } else { + settle_amount.try_floor_u64()? + }; + + let collateral_amount = max_withdraw_amount.try_mul(settle_pct)?; + let withdraw_amount = if collateral_amount == max_withdraw_amount { + collateral_amount.try_ceil_u64()? + } else { + collateral_amount.try_floor_u64()? + }; + + Ok(LiquidateResult { + settle_amount, + repay_amount, + withdraw_amount, + }) } /// Record deposited liquidity and return amount of collateral tokens to mint @@ -363,13 +358,13 @@ pub struct BorrowResult { /// Liquidate obligation result #[derive(Debug)] pub struct LiquidateResult { - /// Amount of collateral to withdraw in exchange for repay amount - pub withdraw_amount: u64, /// 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, } /// Reserve liquidity From 76f61a08ea89224d250644570720c15056e8ad72 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:23:03 -0500 Subject: [PATCH 072/191] last update impl --- token-lending/program/src/processor.rs | 130 +++++++--------- .../program/src/state/last_update.rs | 65 ++++++++ token-lending/program/src/state/mod.rs | 2 + token-lending/program/src/state/obligation.rs | 36 +---- .../src/state/obligation_collateral.rs | 33 +--- .../program/src/state/obligation_liquidity.rs | 33 +--- token-lending/program/src/state/reserve.rs | 144 ++++++++---------- 7 files changed, 210 insertions(+), 233 deletions(-) create mode 100644 token-lending/program/src/state/last_update.rs diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 276c81fb09d..2dedd9f160d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -472,6 +472,9 @@ fn process_deposit_reserve_liquidity( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + if reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -483,8 +486,6 @@ fn process_deposit_reserve_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - assert_last_update_slot(&reserve, clock.slot)?; - let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; @@ -562,6 +563,9 @@ fn process_withdraw_reserve_liquidity( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + if reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -573,8 +577,6 @@ fn process_withdraw_reserve_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - assert_last_update_slot(&reserve, clock.slot)?; - let liquidity_amount = reserve.redeem_collateral(collateral_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; @@ -672,6 +674,9 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } } + if borrow_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -681,12 +686,12 @@ fn process_borrow_obligation_liquidity( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.is_stale(clock.slot)? { + if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } // @TODO: is this enough? other reserves could have been updated that we don't check here, and // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update_slot < borrow_reserve.last_update_slot { + if obligation.last_update < borrow_reserve.last_update { return Err(LendingError::ObligationStale.into()); } @@ -706,16 +711,15 @@ fn process_borrow_obligation_liquidity( if !obligation.liquidity.contains(obligation_liquidity_info.key) { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update_slot < obligation_liquidity.last_update_slot { + if obligation.last_update < obligation_liquidity.last_update { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? - if obligation_liquidity.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -727,9 +731,6 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? - assert_last_update_slot(&borrow_reserve, clock.slot)?; - // @TODO: is this necessary? obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; @@ -742,6 +743,11 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } + let max_borrow_value = obligation + .collateral_value + .try_mul(lending_market_ltv)? + .try_sub(obligation.liquidity_value)?; + let trade_simulator = TradeSimulator::new( dex_market_info, dex_market_orders_info, @@ -752,11 +758,6 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; - let max_borrow_value = obligation - .collateral_value - .try_mul(lending_market_ltv)? - .try_sub(obligation.liquidity_value)?; - let BorrowResult { total_amount, borrow_amount, @@ -881,6 +882,9 @@ fn process_repay_obligation_liquidity( msg!("Invalid destination liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } + if repay_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -890,12 +894,12 @@ fn process_repay_obligation_liquidity( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.is_stale(clock.slot)? { + if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } // @TODO: is this enough? other reserves could have been updated that we don't check here, and // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update_slot < repay_reserve.last_update_slot { + if obligation.last_update < repay_reserve.last_update { return Err(LendingError::ObligationStale.into()); } @@ -915,16 +919,15 @@ fn process_repay_obligation_liquidity( if !obligation.liquidity.contains(obligation_liquidity_info.key) { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update_slot < obligation_liquidity.last_update_slot { + if obligation.last_update < obligation_liquidity.last_update { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? - if obligation_liquidity.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -936,12 +939,6 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? - assert_last_update_slot(&repay_reserve, clock.slot)?; - - // @TODO: is this necessary? - obligation_liquidity.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; - let settle_amount = match liquidity_amount_type { AmountType::ExactAmount => { Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) @@ -1038,6 +1035,9 @@ fn process_liquidate_obligation( msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } + if repay_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { @@ -1054,6 +1054,9 @@ fn process_liquidate_obligation( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + if withdraw_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } // @TODO: are these two checks necessary? // what's the problem with repaying and receiving the same currency? @@ -1077,10 +1080,10 @@ fn process_liquidate_obligation( } // @TODO: is this enough? other reserves could have been updated that we don't check here, and // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update.slot < repay_reserve.last_update.slot { + if obligation.last_update < repay_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update.slot < withdraw_reserve.last_update.slot { + if obligation.last_update < withdraw_reserve.last_update { return Err(LendingError::ObligationStale.into()); } @@ -1100,16 +1103,15 @@ fn process_liquidate_obligation( if !obligation.liquidity.contains(obligation_liquidity_info.key) { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update.slot < obligation_liquidity.last_update.slot { + if obligation.last_update < obligation_liquidity.last_update { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update.slot < obligation_liquidity.last_update.slot above? - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } if obligation_liquidity.borrowed_wads == 0 { // @TODO: make specific to liquidity return Err(LendingError::ObligationEmpty.into()); @@ -1138,16 +1140,15 @@ fn process_liquidate_obligation( { return Err(LendingError::ObligationAccountNotFound.into()); } + if obligation_collateral.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update.slot < obligation_collateral.last_update.slot { + if obligation.last_update < obligation_collateral.last_update { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this necessary if checking obligation.last_update.slot < obligation_liquidity.last_update.slot above? - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } if obligation_collateral.deposited_tokens == 0 { // @TODO: make specific to collateral return Err(LendingError::ObligationEmpty.into()); @@ -1163,13 +1164,6 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? - assert_last_update_slot(&repay_reserve, clock.slot)?; - assert_last_update_slot(&withdraw_reserve, clock.slot)?; - - // @TODO: is this necessary? - obligation_liquidity.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?; - let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); let obligation_ltv = obligation.loan_to_value()?; @@ -1447,12 +1441,12 @@ fn process_withdraw_obligation_collateral( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.is_stale(clock.slot)? { + if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } // @TODO: is this enough? other reserves could have been updated that we don't check here, and // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update_slot < withdraw_reserve.last_update_slot { + if obligation.last_update < withdraw_reserve.last_update { return Err(LendingError::ObligationStale.into()); } @@ -1482,11 +1476,11 @@ fn process_withdraw_obligation_collateral( // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update_slot < obligation_collateral.last_update_slot { + if obligation.last_update < obligation_collateral.last_update { return Err(LendingError::ObligationCollateralStale.into()); } - // @TODO: is this necessary if checking obligation.last_update_slot < obligation_liquidity.last_update_slot above? - if obligation_collateral.is_stale(clock.slot)? { + // @TODO: is this necessary if checking obligation.last_update < obligation_liquidity.last_update above? + if obligation_collateral.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationCollateralStale.into()); } if obligation_collateral.deposited_tokens == 0 { @@ -1826,6 +1820,9 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } } + if deposit_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let mut obligation_collateral = ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; @@ -1847,9 +1844,6 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? collateral exchange rate can change, but interest doesn't accrue - assert_last_update_slot(&deposit_reserve, clock.slot)?; - let trade_simulator = TradeSimulator::new( dex_market_info, dex_market_orders_info, @@ -1921,6 +1915,9 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } } + if borrow_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let mut obligation_liquidity = ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; @@ -1942,9 +1939,6 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? what if we accrue interest here if it's not the current slot? - assert_last_update_slot(&borrow_reserve, clock.slot)?; - let trade_simulator = TradeSimulator::new( dex_market_info, dex_market_orders_info, @@ -2009,7 +2003,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation_collateral.is_stale(clock.slot)? { + if obligation_collateral.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationCollateralStale.into()); } @@ -2033,7 +2027,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> msg!("Invalid obligation account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation_liquidity.is_stale(clock.slot)? { + if obligation_liquidity.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationLiquidityStale.into()); } @@ -2063,14 +2057,6 @@ fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult } } -fn assert_last_update_slot(reserve: &Reserve, slot: Slot) -> ProgramResult { - if !reserve.last_update_slot == slot { - Err(LendingError::ReserveStale.into()) - } else { - Ok(()) - } -} - fn assert_uninitialized( account_info: &AccountInfo, ) -> Result { diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs new file mode 100644 index 00000000000..319f823759f --- /dev/null +++ b/token-lending/program/src/state/last_update.rs @@ -0,0 +1,65 @@ +use crate::{ + error::LendingError, + math::{Decimal, WAD}, +}; +use arrayref::{array_refs, mut_array_refs}; +use solana_program::{ + clock::{Slot, DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, + program_error::ProgramError, + program_option::COption, + pubkey::Pubkey, +}; +use std::cmp::Ordering; + +/// Number of slots to consider stale after +pub const STALE_AFTER_SLOTS: u64 = 10; + +/// Last update state +#[derive(Clone, Debug, Default)] +pub struct LastUpdate { + /// Last slot when updated + pub slot: Slot, +} + +impl LastUpdate { + /// Create new last update + pub fn new() -> Self { + Self { slot: 0 } + } + + /// Return slots elapsed since given slot + pub fn slots_elapsed(&self, slot: Slot) -> Result { + // @FIXME: what should happen if self.slot == 0? + 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; + } + + /// Set last update slot to 0 + pub fn mark_stale(&mut self) { + self.update_slot(0); + } + + /// Check if last update slot is too long ago + pub fn is_stale(&self, slot: Slot) -> Result { + Ok(self.slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) + } +} + +impl PartialEq for LastUpdate { + fn eq(&self, other: &Self) -> bool { + return &self.slot == &other.slot; + } +} + +impl PartialOrd for LastUpdate { + fn partial_cmp(&self, other: &Self) -> Option { + self.slot.partial_cmp(&other.slot) + } +} diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 1434fafdb3d..da33c797ed8 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -1,11 +1,13 @@ //! State types +mod last_update; mod lending_market; mod obligation; mod obligation_collateral; mod obligation_liquidity; mod reserve; +pub use last_update::*; pub use lending_market::*; pub use obligation::*; pub use obligation_collateral::*; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 093262fd385..f91678859b2 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -17,16 +17,13 @@ use std::convert::{TryFrom, TryInto}; /// Max number of collateral and liquidity accounts combined for an obligation pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; -/// Number of slots market values are considered stale after -pub const STALE_AFTER_SLOTS: u64 = 10; - /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { /// Version of the struct pub version: u8, /// Last slot when loan to value updated; set to 0 if collateral or liquidity changed - pub last_update_slot: Slot, + pub last_update: LastUpdate, /// Lending market address pub lending_market: Pubkey, /// Collateral market value in quote currency @@ -60,7 +57,7 @@ impl Obligation { Self { version: PROGRAM_VERSION, - last_update_slot: 0, + last_update: LastUpdate::new(), lending_market, collateral_value: Decimal::zero(), liquidity_value: Decimal::zero(), @@ -73,29 +70,6 @@ impl Obligation { pub fn loan_to_value(&self) -> Result { self.liquidity_value.try_div(self.collateral_value) } - - /// Return slots elapsed since given slot - pub fn slots_elapsed(&self, slot: Slot) -> Result { - let slots_elapsed = slot - .checked_sub(self.last_update_slot) - .ok_or(LendingError::MathOverflow)?; - Ok(slots_elapsed) - } - - /// Set last update slot - pub fn update_slot(&mut self, slot: Slot) { - self.last_update_slot = slot; - } - - /// Set last update slot to 0 - pub fn mark_stale(&mut self) { - self.update_slot(0); - } - - /// Check if last update slot is too long ago - pub fn is_stale(&self, slot: Slot) -> Result { - Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) - } } impl Sealed for Obligation {} @@ -134,7 +108,7 @@ impl Pack for Obligation { ]; *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); pack_decimal(self.collateral_value, collateral_value); pack_decimal(self.liquidity_value, liquidity_value); @@ -207,7 +181,9 @@ impl Pack for Obligation { Ok(Self { version: u8::from_le_bytes(*version), - last_update_slot: u64::from_le_bytes(*last_update_slot), + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + }, lending_market: Pubkey::new_from_array(*lending_market), collateral_value: unpack_decimal(collateral_value), liquidity_value: unpack_decimal(liquidity_value), diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index 7f74fb97b0f..ba109cddb90 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -19,7 +19,7 @@ pub struct ObligationCollateral { /// Version of the obligation collateral pub version: u8, /// Last slot when market value updated; set to 0 if deposited tokens changed - pub last_update_slot: Slot, + pub last_update: LastUpdate, /// Obligation the collateral is associated with pub obligation: Pubkey, /// Reserve which collateral tokens were deposited into @@ -53,7 +53,7 @@ impl ObligationCollateral { Self { version: PROGRAM_VERSION, - last_update_slot: 0, + last_update: LastUpdate::new(), obligation, deposit_reserve, token_mint, @@ -105,29 +105,6 @@ impl ObligationCollateral { .try_mul(obligation_token_supply)? .try_floor_u64() } - - /// Return slots elapsed since given slot - pub fn slots_elapsed(&self, slot: Slot) -> Result { - let slots_elapsed = slot - .checked_sub(self.last_update_slot) - .ok_or(LendingError::MathOverflow)?; - Ok(slots_elapsed) - } - - /// Set last update slot - pub fn update_slot(&mut self, slot: Slot) { - self.last_update_slot = slot; - } - - /// Set last update slot to 0 - pub fn mark_stale(&mut self) { - self.update_slot(0); - } - - /// Check if last update slot is too long ago - pub fn is_stale(&self, slot: Slot) -> Result { - Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) - } } impl Sealed for ObligationCollateral {} @@ -155,7 +132,7 @@ impl Pack for ObligationCollateral { ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); obligation.copy_from_slice(self.obligation.as_ref()); deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); token_mint.copy_from_slice(self.token_mint.as_ref()); @@ -180,7 +157,9 @@ impl Pack for ObligationCollateral { Ok(Self { version: u8::from_le_bytes(*version), - last_update_slot: u64::from_le_bytes(*last_update_slot), + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + }, obligation: Pubkey::new_from_array(*obligation), deposit_reserve: Pubkey::new_from_array(*deposit_reserve), token_mint: Pubkey::new_from_array(*token_mint), diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 1596fc8eb75..2243e4f74ca 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -19,7 +19,7 @@ pub struct ObligationLiquidity { /// Version of the obligation liquidity pub version: u8, /// Last slot when market value and accrued interest updated; set to 0 if borrowed wads changed - pub last_update_slot: Slot, + pub last_update: LastUpdate, /// Obligation the liquidity is associated with pub obligation: Pubkey, /// Reserve which liquidity tokens were borrowed from @@ -50,7 +50,7 @@ impl ObligationLiquidity { Self { version: PROGRAM_VERSION, - last_update_slot: 0, + last_update: LastUpdate::new(), obligation, borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), @@ -97,29 +97,6 @@ impl ObligationLiquidity { self.value = token_converter.convert(self.borrowed_wads, from_token_mint)?; Ok(()) } - - /// Return slots elapsed since given slot - pub fn slots_elapsed(&self, slot: Slot) -> Result { - let slots_elapsed = slot - .checked_sub(self.last_update_slot) - .ok_or(LendingError::MathOverflow)?; - Ok(slots_elapsed) - } - - /// Set last update slot - pub fn update_slot(&mut self, slot: Slot) { - self.last_update_slot = slot; - } - - /// Set last update slot to 0 - pub fn mark_stale(&mut self) { - self.update_slot(0); - } - - /// Check if last update slot is too long ago - pub fn is_stale(&self, slot: Slot) -> Result { - Ok(self.last_update_slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) - } } impl Sealed for ObligationLiquidity {} @@ -147,7 +124,7 @@ impl Pack for ObligationLiquidity { ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); obligation.copy_from_slice(self.obligation.as_ref()); borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); pack_decimal( @@ -175,7 +152,9 @@ impl Pack for ObligationLiquidity { Ok(Self { version: u8::from_le_bytes(*version), - last_update_slot: u64::from_le_bytes(*last_update_slot), + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + }, obligation: Pubkey::new_from_array(*obligation), borrow_reserve: Pubkey::new_from_array(*borrow_reserve), cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 822f376d385..e02e474303d 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -27,7 +27,7 @@ pub struct Reserve { /// Version of the struct pub version: u8, /// Last slot when supply and rates updated - pub last_update_slot: Slot, + pub last_update: LastUpdate, /// Cumulative borrow rate pub cumulative_borrow_rate_wads: Decimal, /// Lending market address @@ -46,7 +46,6 @@ impl Reserve { /// Initialize new reserve state pub fn new(params: NewReserveParams) -> Self { let NewReserveParams { - current_slot, lending_market, collateral: collateral_info, liquidity: liquidity_info, @@ -56,7 +55,7 @@ impl Reserve { Self { version: PROGRAM_VERSION, - last_update_slot: current_slot, + last_update: LastUpdate::new(), cumulative_borrow_rate_wads: Decimal::one(), lending_market, collateral: collateral_info, @@ -281,7 +280,7 @@ impl Reserve { /// Update borrow rate and accrue interest pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult { - let slots_elapsed = self.update_slot(current_slot)?; + let slots_elapsed = self.last_update.slots_elapsed(current_slot)?; if slots_elapsed > 0 { let current_borrow_rate = self.current_borrow_rate()?; let compounded_interest_rate = @@ -290,6 +289,7 @@ impl Reserve { .liquidity .borrowed_amount_wads .try_mul(compounded_interest_rate)?; + self.last_update.update_slot(current_slot); } Ok(()) } @@ -300,16 +300,6 @@ impl Reserve { self.collateral.exchange_rate(total_liquidity) } - // @FIXME: method should match Obligation::update_slot - /// Return slots elapsed since last update - fn update_slot(&mut self, slot: Slot) -> Result { - let slots_elapsed = slot - .checked_sub(self.last_update_slot) - .ok_or(LendingError::MathOverflow)?; - self.last_update_slot = slot; - Ok(slots_elapsed) - } - /// Compound current borrow rate over elapsed slots fn compound_interest( &mut self, @@ -329,8 +319,6 @@ impl Reserve { /// Create new reserve pub struct NewReserveParams { - /// Current slot - pub current_slot: Slot, /// Lending market address pub lending_market: Pubkey, /// Reserve collateral info @@ -615,6 +603,66 @@ const RESERVE_LEN: usize = 600; impl Pack for Reserve { const LEN: usize = RESERVE_LEN; + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, RESERVE_LEN]; + let ( + version, + last_update_slot, + lending_market, + liquidity_mint, + liquidity_mint_decimals, + liquidity_supply, + liquidity_fee_receiver, + collateral_mint, + collateral_supply, + dex_market, + optimal_utilization_rate, + liquidation_bonus, + min_borrow_rate, + optimal_borrow_rate, + max_borrow_rate, + borrow_fee_wad, + host_fee_percentage, + cumulative_borrow_rate_wads, + total_borrows, + available_liquidity, + collateral_mint_supply, + _padding, + ) = mut_array_refs![ + output, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + ]; + *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); + pack_decimal( + self.cumulative_borrow_rate_wads, + cumulative_borrow_rate_wads, + ); + lending_market.copy_from_slice(self.lending_market.as_ref()); + pack_coption_key(&self.dex_market, dex_market); + + // liquidity info + liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); + *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); + liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); + liquidity_fee_receiver.copy_from_slice(self.collateral.fee_receiver.as_ref()); + *available_liquidity = self.liquidity.available_amount.to_le_bytes(); + pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); + + // collateral info + collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); + collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); + *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); + + // config + *optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); + *liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); + *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); + *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); + *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); + *borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); + *host_fee_percentage = self.config.fees.host_fee_percentage.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]; @@ -647,7 +695,9 @@ impl Pack for Reserve { ]; Ok(Self { version: u8::from_le_bytes(*version), - last_update_slot: u64::from_le_bytes(*last_update_slot), + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + }, cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), lending_market: Pubkey::new_from_array(*lending_market), dex_market: unpack_coption_key(dex_market)?, @@ -677,66 +727,6 @@ impl Pack for Reserve { }, }) } - - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN]; - let ( - version, - last_update_slot, - lending_market, - liquidity_mint, - liquidity_mint_decimals, - liquidity_supply, - liquidity_fee_receiver, - collateral_mint, - collateral_supply, - dex_market, - optimal_utilization_rate, - liquidation_bonus, - min_borrow_rate, - optimal_borrow_rate, - max_borrow_rate, - borrow_fee_wad, - host_fee_percentage, - cumulative_borrow_rate_wads, - total_borrows, - available_liquidity, - collateral_mint_supply, - _padding, - ) = mut_array_refs![ - output, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 - ]; - *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update_slot.to_le_bytes(); - pack_decimal( - self.cumulative_borrow_rate_wads, - cumulative_borrow_rate_wads, - ); - lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_coption_key(&self.dex_market, dex_market); - - // liquidity info - liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); - *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); - liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); - liquidity_fee_receiver.copy_from_slice(self.collateral.fee_receiver.as_ref()); - *available_liquidity = self.liquidity.available_amount.to_le_bytes(); - pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); - - // collateral info - collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); - *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); - - // config - *optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); - *liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); - *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); - *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); - *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); - *borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); - *host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); - } } #[cfg(test)] From efdd17a1f52cc35699fbaf4a68d03a4ed0af0f94 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:24:55 -0500 Subject: [PATCH 073/191] renaming --- token-lending/program/src/processor.rs | 15 +++++++-------- token-lending/program/src/state/reserve.rs | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 2dedd9f160d..b1a1acbfd38 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,6 +1,5 @@ //! Program state processor -use crate::state::TokenConverter; use crate::{ dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, @@ -748,7 +747,7 @@ fn process_borrow_obligation_liquidity( .try_mul(lending_market_ltv)? .try_sub(obligation.liquidity_value)?; - let trade_simulator = TradeSimulator::new( + let token_converter = TradeSimulator::new( dex_market_info, dex_market_orders_info, memory, @@ -763,11 +762,11 @@ fn process_borrow_obligation_liquidity( borrow_amount, origination_fee, host_fee, - } = borrow_reserve.borrow( + } = borrow_reserve.borrow_liquidity( liquidity_amount, liquidity_amount_type, max_borrow_value, - trade_simulator, + token_converter, &lending_market.quote_token_mint, )?; @@ -1844,7 +1843,7 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let trade_simulator = TradeSimulator::new( + let token_converter = TradeSimulator::new( dex_market_info, dex_market_orders_info, memory, @@ -1856,7 +1855,7 @@ fn process_refresh_obligation_collateral( obligation_collateral.update_value( deposit_reserve.collateral_exchange_rate()?, - trade_simulator, + token_converter, &deposit_reserve.liquidity.mint_pubkey, )?; obligation_collateral.update_slot(clock.slot)?; @@ -1939,7 +1938,7 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let trade_simulator = TradeSimulator::new( + let token_converter = TradeSimulator::new( dex_market_info, dex_market_orders_info, memory, @@ -1950,7 +1949,7 @@ fn process_refresh_obligation_liquidity( )?; obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; - obligation_liquidity.update_value(trade_simulator, &borrow_reserve.liquidity.mint_pubkey)?; + obligation_liquidity.update_value(token_converter, &borrow_reserve.liquidity.mint_pubkey)?; obligation_liquidity.update_slot(clock.slot)?; ObligationLiquidity::pack( obligation_liquidity, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e02e474303d..83c2ae99e63 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -95,7 +95,7 @@ impl Reserve { } /// Borrow liquidity up to a maximum market value - pub fn borrow( + pub fn borrow_liquidity( &self, liquidity_amount: u64, liquidity_amount_type: AmountType, From bb6f3888728d5f84e55e58fb35496f5178f0e1e8 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:33:50 -0500 Subject: [PATCH 074/191] move repay to reserve --- token-lending/program/src/processor.rs | 14 +++------- token-lending/program/src/state/reserve.rs | 32 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b1a1acbfd38..95d974fcdc5 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -938,16 +938,10 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let settle_amount = match liquidity_amount_type { - AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) - } - AmountType::PercentAmount => { - let settle_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); - settle_pct.try_mul(obligation_liquidity.borrowed_wads)? - }, - }; - let repay_amount = settle_amount.try_floor_u64()?; + let RepayResult { + settle_amount, + repay_amount, + } = repay_reserve.repay_liquidity(liquidity_amount, liquidity_amount_type); repay_reserve.liquidity.repay(repay_amount, settle_amount)?; obligation_liquidity.repay(settle_amount); diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 83c2ae99e63..b40bb608334 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -149,6 +149,28 @@ impl Reserve { } } + pub fn repay_liquidity( + &self, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + ) -> Result { + let settle_amount = match liquidity_amount_type { + AmountType::ExactAmount => { + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + } + AmountType::PercentAmount => { + let settle_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); + obligation_liquidity.borrowed_wads.try_mul(settle_pct)? + } + }; + let repay_amount = settle_amount.try_floor_u64()?; + + Ok(RepayResult { + settle_amount, + repay_amount, + }) + } + /// Liquidate some or all of an unhealthy obligation pub fn liquidate_obligation( &self, @@ -332,6 +354,7 @@ pub struct NewReserveParams { } /// Create borrow result +#[derive(Debug)] pub struct BorrowResult { /// Total amount of borrow plus origination fee pub total_amount: u64, @@ -343,6 +366,15 @@ pub struct BorrowResult { pub host_fee: u64, } +/// Repay liquidity result +#[derive(Debug)] +pub struct RepayResult { + /// Amount of liquidity that is settled from the obligation. + pub settle_amount: Decimal, + /// Amount that will be repaid as u64 + pub repay_amount: u64, +} + /// Liquidate obligation result #[derive(Debug)] pub struct LiquidateResult { From a1eb2c6dbf16ee613aaa7b46fb03415e21443367 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:34:06 -0500 Subject: [PATCH 075/191] format --- token-lending/program/src/processor.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 95d974fcdc5..64f4873925d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1157,8 +1157,7 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - let liquidation_threshold = - Rate::from_percent(lending_market.liquidation_threshold); + let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv < liquidation_threshold { return Err(LendingError::HealthyObligation.into()); @@ -1167,7 +1166,7 @@ fn process_liquidate_obligation( let LiquidateResult { settle_amount, repay_amount, - withdraw_amount + withdraw_amount, } = withdraw_reserve.liquidate_obligation( liquidity_amount, liquidity_amount_type, @@ -1189,8 +1188,14 @@ fn process_liquidate_obligation( obligation.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - ObligationCollateral::pack(obligation_collateral, &mut obligation_collateral_info.data.borrow_mut())?; - ObligationLiquidity::pack(obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut())?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { From d792fa4bd97fda7052125be6d565eb07ace17e07 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:34:39 -0500 Subject: [PATCH 076/191] checked math --- token-lending/program/src/state/reserve.rs | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b40bb608334..f4e4b6fd68d 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -73,22 +73,29 @@ impl Reserve { if low_utilization || self.config.optimal_utilization_rate == 100 { let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?; let min_rate = Rate::from_percent(self.config.min_borrow_rate); - // @FIXME: unchecked math - let rate_range = - Rate::from_percent(self.config.optimal_borrow_rate - self.config.min_borrow_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 { let normalized_rate = utilization_rate .try_sub(optimal_utilization_rate)? - // @FIXME: unchecked math .try_div(Rate::from_percent( - 100 - self.config.optimal_utilization_rate, + 100u8 + .checked_sub(self.config.optimal_utilization_rate) + .ok_or(LendingError::MathOverflow)?, ))?; let min_rate = Rate::from_percent(self.config.optimal_borrow_rate); - // @FIXME: unchecked math - let rate_range = - Rate::from_percent(self.config.max_borrow_rate - self.config.optimal_borrow_rate); + let rate_range = Rate::from_percent( + self.config + .max_borrow_rate + .checked_sub(self.config.optimal_borrow_rate) + .ok_or(LendingError::MathOverflow)?, + ); Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?) } From 74f577770b9b39ff276b9e43bd3f5f87f60f8e42 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 17 Mar 2021 22:34:53 -0500 Subject: [PATCH 077/191] fixme comment [skip-ci] --- token-lending/program/src/processor.rs | 1 + token-lending/program/src/state/reserve.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 64f4873925d..6390ed60ec9 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -757,6 +757,7 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; + // @FIXME: resume here, refactor args for consistency let BorrowResult { total_amount, borrow_amount, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f4e4b6fd68d..f68647ee155 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -101,6 +101,7 @@ impl Reserve { } } + // @FIXME: refactor args /// Borrow liquidity up to a maximum market value pub fn borrow_liquidity( &self, From 35837243bbf0f8dfa1b0bd738ea9e6a1ce6b9c13 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:19:26 -0500 Subject: [PATCH 078/191] refactor Reserve::borrow_liquidity --- token-lending/program/src/processor.rs | 17 ++++++----------- token-lending/program/src/state/reserve.rs | 11 ++++++++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6390ed60ec9..954d83d1b1d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -733,20 +733,15 @@ fn process_borrow_obligation_liquidity( // @TODO: is this necessary? obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; - let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); - let obligation_ltv = obligation.loan_to_value()?; - if obligation_ltv > lending_market_ltv { + let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); + let loan_to_value = obligation.loan_to_value()?; + if loan_to_value > loan_to_value_ratio { return Err(LendingError::ObligationLTVAboveReserveLTV.into()); } - if obligation_ltv == lending_market_ltv { + if loan_to_value == loan_to_value_ratio { return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); } - let max_borrow_value = obligation - .collateral_value - .try_mul(lending_market_ltv)? - .try_sub(obligation.liquidity_value)?; - let token_converter = TradeSimulator::new( dex_market_info, dex_market_orders_info, @@ -757,7 +752,6 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; - // @FIXME: resume here, refactor args for consistency let BorrowResult { total_amount, borrow_amount, @@ -766,7 +760,8 @@ fn process_borrow_obligation_liquidity( } = borrow_reserve.borrow_liquidity( liquidity_amount, liquidity_amount_type, - max_borrow_value, + &obligation, + loan_to_value_ratio, token_converter, &lending_market.quote_token_mint, )?; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f68647ee155..9ec10e23c5f 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -101,16 +101,21 @@ impl Reserve { } } - // @FIXME: refactor args /// Borrow liquidity up to a maximum market value pub fn borrow_liquidity( &self, liquidity_amount: u64, liquidity_amount_type: AmountType, - max_borrow_value: Decimal, + obligation: &Obligation, + loan_to_value_ratio: Rate, token_converter: impl TokenConverter, quote_token_mint: &Pubkey, ) -> Result { + let max_borrow_value = obligation + .collateral_value + .try_mul(loan_to_value_ratio)? + .try_sub(obligation.liquidity_value)?; + match liquidity_amount_type { AmountType::ExactAmount => { let borrow_amount = liquidity_amount; @@ -448,7 +453,7 @@ impl ReserveLiquidity { .ok_or(LendingError::MathOverflow)?; self.borrowed_amount_wads = self .borrowed_amount_wads - .try_add(Decimal::from(borrow_amount))?; + .try_add(borrow_amount.into())?; Ok(()) } From 392f96d3ba318c2c4065351fe65b183c8cfc5459 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:20:22 -0500 Subject: [PATCH 079/191] fixme / formatting [skip ci] --- token-lending/program/src/processor.rs | 3 ++- token-lending/program/src/state/last_update.rs | 1 + token-lending/program/src/state/reserve.rs | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 954d83d1b1d..f92b2047c7e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -766,7 +766,8 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; - // @TODO: will this need further adjustment for fees? + // @FIXME: fees come out of liquidity pool, but they don't get paid by borrower + // @FIXME: don't we need to adjust total supply? borrow_reserve .liquidity .borrow(total_amount, borrow_amount)?; diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index 319f823759f..a479c67edc1 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -41,6 +41,7 @@ impl LastUpdate { self.slot = slot; } + // @FIXME: this will screw up interest rate tracking /// Set last update slot to 0 pub fn mark_stale(&mut self) { self.update_slot(0); diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 9ec10e23c5f..5ddf3f472d9 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -47,8 +47,8 @@ impl Reserve { pub fn new(params: NewReserveParams) -> Self { let NewReserveParams { lending_market, - collateral: collateral_info, - liquidity: liquidity_info, + collateral, + liquidity, dex_market, config, } = params; @@ -58,8 +58,8 @@ impl Reserve { last_update: LastUpdate::new(), cumulative_borrow_rate_wads: Decimal::one(), lending_market, - collateral: collateral_info, - liquidity: liquidity_info, + collateral, + liquidity, dex_market, config, } From d7daabcfe7bda12ee1d42956ac7daecae303deca Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:42:21 -0500 Subject: [PATCH 080/191] fix Reserve::repay_liquidity --- token-lending/program/src/processor.rs | 6 +++++- token-lending/program/src/state/reserve.rs | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f92b2047c7e..b9c9a189327 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -938,7 +938,11 @@ fn process_repay_obligation_liquidity( let RepayResult { settle_amount, repay_amount, - } = repay_reserve.repay_liquidity(liquidity_amount, liquidity_amount_type); + } = repay_reserve.repay_liquidity( + liquidity_amount, + liquidity_amount_type, + &obligation_liquidity, + ); repay_reserve.liquidity.repay(repay_amount, settle_amount)?; obligation_liquidity.repay(settle_amount); diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 5ddf3f472d9..cbd24844f1c 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -166,6 +166,7 @@ impl Reserve { &self, liquidity_amount: u64, liquidity_amount_type: AmountType, + obligation_liquidity: &ObligationLiquidity, ) -> Result { let settle_amount = match liquidity_amount_type { AmountType::ExactAmount => { @@ -176,7 +177,11 @@ impl Reserve { obligation_liquidity.borrowed_wads.try_mul(settle_pct)? } }; - let repay_amount = settle_amount.try_floor_u64()?; + let repay_amount = if settle_amount == obligation_liquidity.borrowed_wads { + settle_amount.try_ceil_u64()? + } else { + settle_amount.try_floor_u64()? + }; Ok(RepayResult { settle_amount, From eb4e82ccfceb31714239cefd031898a980c5d2d2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:42:56 -0500 Subject: [PATCH 081/191] .mark_stale() -> .last_update.mark_stale() --- token-lending/program/src/processor.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b9c9a189327..86e2da6735c 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -773,8 +773,8 @@ fn process_borrow_obligation_liquidity( .borrow(total_amount, borrow_amount)?; // @TODO: will this need further adjustment for fees? obligation_liquidity.borrow(borrow_amount); - obligation_liquidity.mark_stale(); - obligation.mark_stale(); + obligation_liquidity.last_update.mark_stale(); + obligation.last_update.mark_stale(); ObligationLiquidity::pack( obligation_liquidity, @@ -946,8 +946,8 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; obligation_liquidity.repay(settle_amount); - obligation_liquidity.mark_stale(); - obligation.mark_stale(); + obligation_liquidity.last_update.mark_stale(); + obligation.last_update.mark_stale(); ObligationLiquidity::pack( obligation_liquidity, @@ -1184,9 +1184,9 @@ fn process_liquidate_obligation( obligation_liquidity.repay(settle_amount); // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? obligation_collateral.withdraw(withdraw_amount); - obligation_liquidity.mark_stale(); - obligation_collateral.mark_stale(); - obligation.mark_stale(); + obligation_liquidity.last_update.mark_stale(); + obligation_collateral.last_update.mark_stale(); + obligation.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; ObligationCollateral::pack( @@ -1346,8 +1346,8 @@ fn process_deposit_obligation_collateral( } obligation_collateral.deposit(collateral_amount)?; - obligation_collateral.mark_stale(); - obligation.mark_stale(); + obligation_collateral.last_update.mark_stale(); + obligation.last_update.mark_stale(); ObligationCollateral::pack( obligation_collateral, @@ -1552,8 +1552,8 @@ fn process_withdraw_obligation_collateral( // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? obligation_collateral.withdraw(withdraw_amount)?; - obligation_collateral.mark_stale(); - obligation.mark_stale(); + obligation_collateral.last_update.mark_stale(); + obligation.last_update.mark_stale(); ObligationCollateral::pack( obligation_collateral, @@ -1682,7 +1682,7 @@ fn process_init_obligation_collateral( token_mint: obligation_token_mint_info.key(), }); obligation.collateral.push(*obligation_collateral_info.key); - obligation.mark_stale(); + obligation.last_update.mark_stale(); ObligationCollateral::pack( obligation_collateral, @@ -1763,7 +1763,7 @@ fn process_init_obligation_liquidity( borrow_reserve: *borrow_reserve_info.key, }); obligation.liquidity.push(*obligation_liquidity_info.key); - obligation.mark_stale(); + obligation.last_update.mark_stale(); ObligationLiquidity::pack( obligation_liquidity, From f015c6684c4e24fc9cce1ece8e993b185c86d5e6 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:49:58 -0500 Subject: [PATCH 082/191] fee calc enum --- token-lending/program/src/state/reserve.rs | 29 ++++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index cbd24844f1c..e184f7368c4 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -122,7 +122,7 @@ impl Reserve { let (origination_fee, host_fee) = self .config .fees - .calculate_borrow_fees(borrow_amount, false)?; + .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; let total_amount = borrow_amount .checked_add(origination_fee) .ok_or(LendingError::MathOverflow)?; @@ -146,8 +146,10 @@ impl Reserve { .convert(total_value, "e_token_mint)? .try_floor_u64()? .min(self.liquidity.available_amount); - let (origination_fee, host_fee) = - self.config.fees.calculate_borrow_fees(total_amount, true)?; + let (origination_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(total_amount, FeeCalculation::Inclusive)?; let borrow_amount = total_amount .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; @@ -596,13 +598,20 @@ pub struct ReserveFees { pub host_fee_percentage: u8, } +/// 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 ReserveFees { /// Calculate the owner and host fees on borrow pub fn calculate_borrow_fees( &self, borrow_amount: u64, - // @FIXME: use enum - inclusive: bool, + fee_calculation: FeeCalculation, ) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); @@ -614,13 +623,11 @@ impl ReserveFees { 1 // 1 token to owner, nothing else }; - let borrow_fee_amount = if inclusive { - // inclusive fee = (rate / (1 + rate)) * amount - borrow_fee_rate + let borrow_fee_amount = match fee_calculation { + FeeCalculation::Exclusive => borrow_fee_rate.try_mul(borrow_amount)?, + FeeCalculation::Inclusive => borrow_fee_rate .try_div(borrow_fee_rate.try_add(Rate::one())?)? - .try_mul(borrow_amount)? - } else { - borrow_fee_rate.try_mul(borrow_amount)? + .try_mul(borrow_amount)?, }; let borrow_fee = borrow_fee_amount.try_round_u64()?.max(minimum_fee); From 96dfed21e6b54cec7f67b86d58f90f786f6834b2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 13:50:47 -0500 Subject: [PATCH 083/191] comment / fmt [skip-ci] --- token-lending/program/src/state/reserve.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e184f7368c4..92e3e7baeb6 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -458,9 +458,7 @@ impl ReserveLiquidity { .available_amount .checked_sub(total_amount) .ok_or(LendingError::MathOverflow)?; - self.borrowed_amount_wads = self - .borrowed_amount_wads - .try_add(borrow_amount.into())?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; Ok(()) } @@ -568,7 +566,7 @@ impl From for Rate { pub struct ReserveConfig { /// Optimal utilization rate as a percent pub optimal_utilization_rate: u8, - /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation + /// The percent bonus the liquidator gets when repaying liquidity to an unhealthy obligation pub liquidation_bonus: u8, /// Min borrow APY pub min_borrow_rate: u8, From ce97e241f5c9d8358592b4607d699b6e34f86833 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 18:07:46 -0500 Subject: [PATCH 084/191] major reorg [skip ci] --- token-lending/program/src/error.rs | 168 +- token-lending/program/src/instruction.rs | 860 +++++---- token-lending/program/src/processor.rs | 1678 ++++++++--------- .../program/src/state/last_update.rs | 15 +- token-lending/program/src/state/obligation.rs | 86 +- .../src/state/obligation_collateral.rs | 17 +- .../program/src/state/obligation_liquidity.rs | 17 +- token-lending/program/src/state/reserve.rs | 218 ++- .../tests/withdraw_obligation_collateral.rs | 4 +- 9 files changed, 1586 insertions(+), 1477 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index c0314daccd7..bc7e3481395 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -7,6 +7,7 @@ use thiserror::Error; /// Errors that may be returned by the TokenLending program. #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum LendingError { + // @FIXME: reorganize // 0 /// Invalid instruction data passed in. #[error("Failed to unpack instruction data")] @@ -54,121 +55,120 @@ pub enum LendingError { /// Math operation overflow #[error("Math operation overflow")] MathOverflow, - /// Negative interest rate - #[error("Interest rate is negative")] - NegativeInterestRate, - - // 15 /// Memory is too small #[error("Memory is too small")] MemoryTooSmall, + + // 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, + + // 15 /// The reserve lending market must be the same #[error("Reserve mints do not match dex market mints")] DexMarketMintMismatch, - /// The reserve lending market must be the same - #[error("Reserve lending market mismatch")] - LendingMarketMismatch, - /// The obligation token owner must be the same if reusing an obligation - #[error("Obligation token owner mismatch")] - ObligationTokenOwnerMismatch, - /// Insufficient liquidity available - #[error("Insufficient liquidity available")] - InsufficientLiquidity, + /// Trade simulation error + #[error("Trade simulation error")] + TradeSimulationError, + /// Invalid dex order book side + #[error("Invalid dex order book side")] + DexInvalidOrderBookSide, - // 20 - /// This reserve's collateral cannot be used for borrows - #[error("Input reserve has collateral disabled")] - ReserveCollateralDisabled, + // @TODO: these are only used in one place that might be removed /// Input reserves cannot be the same #[error("Input reserves cannot be the same")] DuplicateReserve, /// Input reserves cannot use the same liquidity mint #[error("Input reserves cannot use the same liquidity mint")] DuplicateReserveMint, - // @TODO: make this specific to collateral - /// Obligation amount is empty - #[error("Obligation amount is empty")] - ObligationEmpty, - /// Cannot liquidate healthy obligations - #[error("Cannot liquidate healthy obligations")] - HealthyObligation, + + // 20 + // @FIXME: reserve needs a new field to support this + /// This reserve's collateral cannot be used for borrows + #[error("Input reserve has collateral disabled")] + ReserveCollateralDisabled, + /// Insufficient liquidity available + #[error("Insufficient liquidity available")] + InsufficientLiquidity, + /// Reserve state stale + #[error("Reserve state needs to be refreshed")] + ReserveStale, // 25 /// Borrow amount too small - #[error("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, /// Liquidation amount too small #[error("Liquidation amount too small to receive collateral")] LiquidationTooSmall, - /// Reserve state stale - #[error("Reserve state needs to be updated for the current slot")] - ReserveStale, - /// Trade simulation error - #[error("Trade simulation error")] - TradeSimulationError, - /// Invalid dex order book side - #[error("Invalid dex order book side")] - DexInvalidOrderBookSide, - - // 30 - /// 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, + /// Cannot liquidate healthy obligations + #[error("Cannot liquidate healthy obligations")] + ObligationHealthy, - // @FIXME: split up after 35 // 35 - // @FIXME: change name + message - /// ObligationAccountLimit - #[error("ObligationAccountLimit")] + /// Obligation account limit reached + #[error("Obligation account limit reached")] ObligationAccountLimit, + /// Obligation state stale + #[error("Obligation state needs to be refreshed")] + ObligationStale, + // @FIXME: not used anywhere, maybe should be? + /// The obligation token owner must be the same if reusing an obligation + #[error("Obligation token owner mismatch")] + ObligationTokenOwnerMismatch, // @FIXME: change name + message - /// ObligationAccountDuplicate - #[error("ObligationAccountDuplicate")] - ObligationAccountDuplicate, - // @FIXME: change name + message - /// ObligationAccountNotFound - #[error("ObligationAccountNotFound")] - ObligationAccountNotFound, + /// Obligation LTV is above the lending market LTV + #[error("Obligation LTV is above the lending market LTV")] + ObligationLTVAboveLendingMarketLTV, // @FIXME: change name + message + /// Obligation LTV cannot go above the lending market LTV + #[error("Obligation LTV cannot go above the lending market LTV")] + ObligationLTVCannotGoAboveLendingMarketLTV, + + /// Obligation collateral is empty + #[error("Obligation collateral is empty")] + ObligationCollateralEmpty, + /// Obligation collateral stale #[error("Obligation collateral state needs to be refreshed")] ObligationCollateralStale, + /// Obligation collateral withdraw too large + #[error("Obligation collateral withdraw too large for borrowed liquidity")] + ObligationCollateralWithdrawTooLarge, // @FIXME: change name + message + /// ObligationCollateralDuplicate + #[error("ObligationCollateralDuplicate")] + ObligationCollateralDuplicate, + + /// Obligation liquidity is empty + #[error("Obligation liquidity is empty")] + ObligationLiquidityEmpty, + /// Obligation liquidity stale #[error("Obligation liquidity state needs to be refreshed")] ObligationLiquidityStale, // @FIXME: change name + message - #[error("Obligation state needs to be refreshed")] - ObligationStale, - // @FIXME: change name + message - /// Invalid obligation collateral amount - #[error("Invalid obligation collateral amount")] - InvalidObligationCollateral, - // @FIXME: change name + message - /// Obligation LTV is above the reserve LTV - #[error("Obligation LTV is above the reserve LTV")] - ObligationLTVAboveReserveLTV, - // @FIXME: change name + message - /// Obligation LTV cannot go above the reserve LTV - #[error("Obligation LTV cannot go above the reserve LTV")] - ObligationLTVCannotGoAboveReserveLTV, - // @FIXME: change name + message - /// Obligation collateral cannot be withdrawn below required amount - #[error("Obligation collateral cannot be withdrawn below required amount")] - ObligationCollateralWithdrawBelowRequired, - /// Borrow amount too small - #[error("Borrow amount too large")] - BorrowTooLarge, + /// ObligationLiquidityDuplicate + #[error("ObligationLiquidityDuplicate")] + ObligationLiquidityDuplicate, + + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, } impl From for ProgramError { diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 6d45f2709f9..bbb91bce3f8 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -5,6 +5,7 @@ use crate::{ state::{ReserveConfig, ReserveFees}, }; use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, @@ -13,7 +14,7 @@ use solana_program::{ }; use std::{convert::TryInto, mem::size_of}; -/// Describe how the borrow input amount should be treated +/// Describe how an input amount should be treated #[derive(Clone, Copy, Debug, PartialEq, FromPrimitive, ToPrimitive)] pub enum AmountType { /// Treat amount as an exact amount of tokens @@ -44,6 +45,18 @@ pub enum LendingInstruction { }, // 1 + /// Sets the new owner of a lending market. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Lending market account + /// 1. `[signer]` Current owner + SetLendingMarketOwner { + /// The new owner + new_owner: Pubkey, + }, + + // 2 /// Initializes a new lending market reserve. /// /// Accounts expected by this instruction: @@ -74,21 +87,9 @@ pub enum LendingInstruction { config: ReserveConfig, }, - // 2 - /// Initializes a new loan obligation. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account - uninitialized - /// 1. `[]` Lending market account - /// 2. `[]` Rent sysvar - /// 3. `[]` Token program id - InitObligation, - // 3 - // @TODO: consider renaming to SwapReserveLiquidityForCollateral - /// Deposit liquidity into a reserve. The output is a collateral token representing ownership - /// of the reserve liquidity pool. + /// Deposit liquidity into a reserve in exchange for collateral representing ownership of the + /// reserve liquidity pool. /// /// Accounts expected by this instruction: /// @@ -109,9 +110,7 @@ pub enum LendingInstruction { }, // 4 - // @TODO: consider renaming to SwapReserveCollateralForLiquidity or RedeemReserveCollateral - /// Withdraw tokens from a reserve. The input is a collateral token representing ownership - /// of the reserve liquidity pool. + /// Redeem collateral from a reserve in exchange for liquidity. /// /// Accounts expected by this instruction: /// @@ -125,111 +124,84 @@ pub enum LendingInstruction { /// 6. `[]` Derived lending market authority /// 7. `[signer]` User transfer authority ($authority) /// 8. `[]` Token program id - WithdrawReserveLiquidity { + RedeemReserveCollateral { /// Amount of collateral to return in exchange for liquidity collateral_amount: u64, }, // 5 - // @FIXME: update description - /// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens - /// is calculated by market price. Requires a recently refreshed obligation. Obligation - /// liquidity must be initialized. + /// Accrue interest on reserves /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account - /// 1. `[writable]` Destination liquidity token account - /// Minted by borrow reserve liquidity mint. - /// 2. `[writable]` Borrow reserve account - /// 3. `[writable]` Borrow reserve liquidity fee receiver account - /// Must be the fee account specified at InitReserve. - /// 4. `[writable]` Obligation account - /// 5. `[writable]` Obligation liquidity account - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[]` Dex market - /// 9. `[]` Dex market order book side - /// 10 `[]` Temporary memory - /// 11 `[]` Clock sysvar - /// 12 `[]` Token program id - /// 13 `[optional, writable]` Host fee receiver account - BorrowObligationLiquidity { - // TODO: slippage constraint - /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` - liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, - }, + /// 0. `[]` Clock sysvar + /// 1. `[writable]` Reserve account + /// .. `[writable]` Additional reserve accounts + AccrueReserveInterest, // 6 - // @TODO: update docs - /// Repay loaned tokens to a reserve. The obligation balance will be recalculated for interest. + /// Initializes a new lending market obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account - /// Minted by repay reserve liquidity mint. - /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account - /// 2. `[writable]` Repay reserve account - /// 3. `[writable]` Obligation account - /// 4. `[writable]` Obligation liquidity account - /// 5. `[]` Lending market account - /// 6. `[]` Derived lending market authority - /// 7. `[signer]` User transfer authority ($authority) - /// 8. `[]` Clock sysvar - /// 9. `[]` Token program id - RepayObligationLiquidity { - /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` - liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, - }, + /// 0. `[writable]` Obligation account - uninitialized + /// 1. `[]` Lending market account + /// 2. `[]` Clock sysvar + /// 3. `[]` Rent sysvar + /// 4. `[]` Token program id + InitObligation, // 7 - // @FIXME - /// Purchase collateral tokens at a discount rate from an unhealthy obligation. Requires a - /// recently refreshed obligation. + /// Refresh an obligation's loan to value ratio. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account - /// Minted by repay reserve liquidity mint. - /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination collateral token account - /// Minted by withdraw reserve collateral mint - /// 2. `[writable]` Repay reserve account - /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[writable]` Withdraw reserve account - /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation account - /// 7. `[writable]` Obligation liquidity account - /// 8. `[writable]` Obligation collateral account - /// 9. `[]` Lending market account - /// 10 `[]` Derived lending market authority - /// 11 `[signer]` User transfer authority ($authority) - /// 12 `[]` Clock sysvar - /// 13 `[]` Token program id - LiquidateObligation { - /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` - liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, - }, + /// 0. `[writable]` Obligation account + /// 1. `[]` Lending market account + /// 2. `[]` Clock sysvar + /// 3. `[]` Token program id + /// 4..4+N `[]` Obligation collateral and liquidity accounts + /// Must be all initialized collateral accounts in exact order, followed by + /// all initialized liquidity accounts in exact order, with no additional + /// accounts following. + RefreshObligation, // 8 - /// Accrue interest on reserves + /// Initializes a new obligation collateral. /// /// Accounts expected by this instruction: /// - /// 0. `[]` Clock sysvar - /// 1. `[writable]` Reserve account - /// .. `[writable]` Additional reserve accounts - AccrueReserveInterest, + /// 0. `[writable]` Obligation account + /// 1. `[writable]` Obligation collateral account - uninitialized + /// 2. `[]` Deposit reserve account + /// 3. `[writable]` Obligation token mint - uninitialized + /// 4. `[writable]` Obligation token output account + /// 5. `[]` Obligation token owner + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[]` Clock sysvar + /// 9. `[]` Rent sysvar + /// 10 `[]` Token program id + InitObligationCollateral, // 9 - /// Deposit collateral to an obligation. Obligation collateral must be initialized. + /// Refresh market value of an obligation's collateral. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation collateral account + /// 1. `[]` Deposit reserve account + /// 2. `[]` Lending market account + /// 3. `[]` Derived lending market authority + /// 4. `[]` Dex market + /// 5. `[]` Dex market order book side + /// 6. `[]` Temporary memory + /// 7. `[]` Clock sysvar + /// 8. `[]` Token program id + RefreshObligationCollateral, + + // 10 + /// Deposit collateral to an obligation. /// /// Accounts expected by this instruction: /// @@ -251,8 +223,8 @@ pub enum LendingInstruction { collateral_amount: u64, }, - // 10 - /// Withdraw excess collateral from an obligation. The loan must remain healthy. Requires a + // 11 + /// Withdraw collateral from an obligation. The loan must remain healthy. Requires a /// recently refreshed obligation. /// /// Accounts expected by this instruction: @@ -278,36 +250,7 @@ pub enum LendingInstruction { collateral_amount_type: AmountType, }, - // 11 - /// Sets the new owner of a lending market. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Lending market account - /// 1. `[signer]` Current owner - SetLendingMarketOwner { - /// The new owner - new_owner: Pubkey, - }, - // 12 - /// Initializes a new obligation collateral. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account - /// 1. `[writable]` Obligation collateral account - uninitialized - /// 2. `[]` Deposit reserve account - /// 3. `[writable]` Obligation token mint - uninitialized - /// 4. `[writable]` Obligation token output account - /// 5. `[]` Obligation token owner - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[]` Rent sysvar - /// 9. `[]` Token program id - InitObligationCollateral, - - // 13 /// Initializes a new obligation liquidity. /// /// Accounts expected by this instruction: @@ -316,17 +259,18 @@ pub enum LendingInstruction { /// 1. `[writable]` Obligation liquidity account - uninitialized /// 2. `[]` Borrow reserve account /// 3. `[]` Lending market account - /// 4. `[]` Rent sysvar - /// 5. `[]` Token program id + /// 4. `[]` Clock sysvar + /// 5. `[]` Rent sysvar + /// 6. `[]` Token program id InitObligationLiquidity, - // 14 - /// Refresh market value of an obligation's collateral. + // 13 + /// Refresh market value and accrued interest of an obligation's liquidity. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation collateral account - /// 1. `[]` Deposit reserve account + /// 0. `[writable]` Obligation liquidity account + /// 1. `[]` Borrow reserve account /// 2. `[]` Lending market account /// 3. `[]` Derived lending market authority /// 4. `[]` Dex market @@ -334,38 +278,91 @@ pub enum LendingInstruction { /// 6. `[]` Temporary memory /// 7. `[]` Clock sysvar /// 8. `[]` Token program id - RefreshObligationCollateral, + RefreshObligationLiquidity, + + // 14 + /// Borrow liquidity from a reserve by depositing collateral tokens. The amount of liquidity is + /// determined by market price. Requires a recently refreshed obligation. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account + /// 1. `[writable]` Destination liquidity token account + /// Minted by borrow reserve liquidity mint. + /// 2. `[writable]` Borrow reserve account + /// 3. `[writable]` Borrow reserve liquidity fee receiver account + /// Must be the fee account specified at InitReserve. + /// 4. `[writable]` Obligation account + /// 5. `[writable]` Obligation liquidity account + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[]` Dex market + /// 9. `[]` Dex market order book side + /// 10 `[]` Temporary memory + /// 11 `[]` Clock sysvar + /// 12 `[]` Token program id + /// 13 `[optional, writable]` Host fee receiver account + BorrowObligationLiquidity { + /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` + liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, + // TODO: slippage constraint - https://git.io/JmV67 + }, // 15 - /// Refresh market value and accrued interest of an obligation's liquidity. + /// Repay borrowed liquidity to a reserve. Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation liquidity account - /// 1. `[]` Borrow reserve account - /// 2. `[]` Lending market account - /// 3. `[]` Derived lending market authority - /// 4. `[]` Dex market - /// 5. `[]` Dex market order book side - /// 6. `[]` Temporary memory - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program id - RefreshObligationLiquidity, + /// 0. `[writable]` Source liquidity token account + /// Minted by repay reserve liquidity mint. + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account + /// 2. `[writable]` Repay reserve account + /// 3. `[writable]` Obligation account + /// 4. `[writable]` Obligation liquidity account + /// 5. `[]` Lending market account + /// 6. `[]` Derived lending market authority + /// 7. `[signer]` User transfer authority ($authority) + /// 8. `[]` Clock sysvar + /// 9. `[]` Token program id + RepayObligationLiquidity { + /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` + liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, + }, // 16 - /// Refresh an obligation's loan to value ratio. + /// Repay borrowed liquidity to a reserve to receive collateral at a discount from an unhealthy + /// obligation. Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation account - /// 1. `[]` Lending market account - /// 2. `[]` Clock sysvar - /// 3. `[]` Token program id - /// 4..4+N `[]` Obligation collateral and liquidity accounts - /// Must be all initialized collateral accounts in exact order, followed by - /// all initialized liquidity accounts in exact order, with no additional - /// accounts following. - RefreshObligation, + /// 0. `[writable]` Source liquidity token account + /// Minted by repay reserve liquidity mint. + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination collateral token account + /// Minted by withdraw reserve collateral mint + /// 2. `[writable]` Repay reserve account + /// 3. `[writable]` Repay reserve liquidity supply SPL Token account + /// 4. `[writable]` Withdraw reserve account + /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account + /// 6. `[writable]` Obligation account + /// 7. `[writable]` Obligation liquidity account + /// 8. `[writable]` Obligation collateral account + /// 9. `[]` Lending market account + /// 10 `[]` Derived lending market authority + /// 11 `[signer]` User transfer authority ($authority) + /// 12 `[]` Clock sysvar + /// 13 `[]` Token program id + LiquidateObligation { + /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` + liquidity_amount: u64, + /// Describe how `liquidity_amount` should be treated + liquidity_amount_type: AmountType, + }, } impl LendingInstruction { @@ -386,6 +383,10 @@ impl LendingInstruction { } } 1 => { + let (new_owner, _rest) = Self::unpack_pubkey(rest)?; + Self::SetLendingMarketOwner { new_owner } + } + 2 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; @@ -409,51 +410,65 @@ impl LendingInstruction { }, } } - 2 => Self::InitObligation, 3 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::DepositReserveLiquidity { liquidity_amount } } 4 => { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::WithdrawReserveLiquidity { collateral_amount } + Self::RedeemReserveCollateral { collateral_amount } } - 5 => { - let (amount, rest) = Self::unpack_u64(rest)?; - let (amount_type, _rest) = Self::unpack_u8(rest)?; - let amount_type = - AmountType::from_u8(amount_type).ok_or(LendingError::InstructionUnpackError)?; + 5 => Self::AccrueReserveInterest, + 6 => Self::InitObligation, + 7 => Self::RefreshObligation, + 8 => Self::InitObligationCollateral, + 9 => Self::RefreshObligationCollateral, + 10 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::DepositObligationCollateral { collateral_amount } + } + 11 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + let (collateral_amount_type, _rest) = Self::unpack_u8(rest)?; + let collateral_amount_type = AmountType::from_u8(collateral_amount_type) + .ok_or(LendingError::InstructionUnpackError)?; + Self::WithdrawObligationCollateral { + collateral_amount, + collateral_amount_type, + } + } + 12 => Self::InitObligationLiquidity, + 13 => Self::RefreshObligationLiquidity, + 14 => { + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; + let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; + let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) + .ok_or(LendingError::InstructionUnpackError)?; Self::BorrowObligationLiquidity { - liquidity_amount: amount, - liquidity_amount_type: amount_type, + liquidity_amount, + liquidity_amount_type, } } - 6 => { + 15 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; - Self::RepayObligationLiquidity { liquidity_amount } + let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; + let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) + .ok_or(LendingError::InstructionUnpackError)?; + Self::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } } - 7 => { + 16 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; - Self::LiquidateObligation { liquidity_amount } - } - 8 => Self::AccrueReserveInterest, - 9 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::DepositObligationCollateral { collateral_amount } - } - 10 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::WithdrawObligationCollateral { collateral_amount } - } - 11 => { - let (new_owner, _rest) = Self::unpack_pubkey(rest)?; - Self::SetLendingMarketOwner { new_owner } + let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; + let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) + .ok_or(LendingError::InstructionUnpackError)?; + Self::LiquidateObligation { + liquidity_amount, + liquidity_amount_type, + } } - 12 => Self::InitObligationCollateral, - 13 => Self::InitObligationLiquidity, - 14 => Self::RefreshObligationCollateral, - 15 => Self::RefreshObligationLiquidity, - 16 => Self::RefreshObligation, _ => return Err(LendingError::InstructionUnpackError.into()), }) } @@ -510,6 +525,10 @@ impl LendingInstruction { buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); } + Self::SetLendingMarketOwner { new_owner } => { + buf.push(1); + buf.extend_from_slice(new_owner.as_ref()); + } Self::InitReserve { liquidity_amount, config: @@ -526,7 +545,7 @@ impl LendingInstruction { }, }, } => { - buf.push(1); + buf.push(2); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&optimal_utilization_rate.to_le_bytes()); buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); @@ -536,70 +555,70 @@ impl LendingInstruction { buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } - Self::InitObligation => { - buf.push(2); - } Self::DepositReserveLiquidity { liquidity_amount } => { buf.push(3); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::WithdrawReserveLiquidity { collateral_amount } => { + Self::RedeemReserveCollateral { collateral_amount } => { buf.push(4); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + Self::AccrueReserveInterest => { buf.push(5); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + Self::InitObligation => { buf.push(6); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::LiquidateObligation { liquidity_amount } => { + Self::RefreshObligation => { buf.push(7); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::AccrueReserveInterest => { + Self::InitObligationCollateral => { buf.push(8); } - Self::DepositObligationCollateral { collateral_amount } => { + Self::RefreshObligationCollateral => { buf.push(9); + } + Self::DepositObligationCollateral { collateral_amount } => { + buf.push(10); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } Self::WithdrawObligationCollateral { collateral_amount, collateral_amount_type, } => { - buf.push(10); + buf.push(11); buf.extend_from_slice(&collateral_amount.to_le_bytes()); buf.extend_from_slice(&collateral_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::SetLendingMarketOwner { new_owner } => { - buf.push(11); - buf.extend_from_slice(new_owner.as_ref()); - } - Self::InitObligationCollateral => { + Self::InitObligationLiquidity => { buf.push(12); } - Self::InitObligationLiquidity => { + Self::RefreshObligationLiquidity => { buf.push(13); } - Self::RefreshObligationCollateral => { + Self::BorrowObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } => { buf.push(14); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RefreshObligationLiquidity => { + Self::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } => { buf.push(15); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RefreshObligation => { + Self::LiquidateObligation { + liquidity_amount, + liquidity_amount_type, + } => { buf.push(16); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } } buf @@ -632,6 +651,23 @@ pub fn init_lending_market( } } +/// Creates a 'SetLendingMarketOwner' instruction. +pub fn set_lending_market_owner( + program_id: Pubkey, + lending_market_pubkey: Pubkey, + lending_market_owner: Pubkey, + new_owner: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_owner, true), + ], + data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), + } +} + /// Creates an 'InitReserve' instruction. #[allow(clippy::too_many_arguments)] pub fn init_reserve( @@ -684,25 +720,6 @@ pub fn init_reserve( } } -/// Creates an 'InitObligation' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_obligation( - program_id: Pubkey, - obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, -) -> Instruction { - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::InitObligation.pack(), - } -} - /// Creates a 'DepositReserveLiquidity' instruction. #[allow(clippy::too_many_arguments)] pub fn deposit_reserve_liquidity( @@ -736,9 +753,9 @@ pub fn deposit_reserve_liquidity( } } -/// Creates a 'WithdrawReserveLiquidity' instruction. +/// Creates a 'RedeemReserveCollateral' instruction. #[allow(clippy::too_many_arguments)] -pub fn withdraw_reserve_liquidity( +pub fn redeem_reserve_collateral( program_id: Pubkey, collateral_amount: u64, source_collateral_pubkey: Pubkey, @@ -765,156 +782,133 @@ pub fn withdraw_reserve_liquidity( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::WithdrawReserveLiquidity { collateral_amount }.pack(), + data: LendingInstruction::RedeemReserveCollateral { collateral_amount }.pack(), } } -/// Creates a 'BorrowObligationLiquidity' instruction. +/// Creates an `AccrueReserveInterest` instruction +pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { + let mut accounts = Vec::with_capacity(1 + reserve_pubkeys.len()); + accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)); + accounts.extend( + reserve_pubkeys + .into_iter() + .map(|reserve_pubkey| AccountMeta::new(reserve_pubkey, false)), + ); + Instruction { + program_id, + accounts, + data: LendingInstruction::AccrueReserveInterest.pack(), + } +} + +/// Creates an 'InitObligation' instruction. #[allow(clippy::too_many_arguments)] -pub fn borrow_obligation_liquidity( +pub fn init_obligation( program_id: Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, lending_market_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, - host_fee_receiver_pubkey: Option, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); - let mut accounts = vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(borrow_reserve_pubkey, false), - AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::InitObligation.pack(), + } +} + +/// Creates a 'RefreshObligation' instruction. +#[allow(clippy::too_many_arguments)] +pub fn refresh_obligation( + program_id: Pubkey, + obligation_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + obligation_collateral_liquidity_pubkeys: Vec, +) -> Instruction { + let mut accounts = Vec::with_capacity(4 + obligation_collateral_liquidity_pubkeys.len()); + accounts.extend(vec![ + AccountMeta::new(obligation_pubkey, false) AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), - ]; - if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { - accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); - } + ]); + accounts.extend( + obligation_collateral_liquidity_pubkeys + .into_iter() + .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + ); Instruction { program_id, accounts, - data: LendingInstruction::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } - .pack(), + data: LendingInstruction::RefreshObligationLiquidity.pack(), } } -/// Creates a `RepayObligationLiquidity` instruction +/// Creates an 'InitObligationCollateral' instruction. #[allow(clippy::too_many_arguments)] -pub fn repay_obligation_liquidity( +pub fn init_obligation_collateral( program_id: Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, + deposit_reserve_pubkey: Pubkey, + obligation_token_mint_pubkey: Pubkey, + obligation_token_output_pubkey: Pubkey, + obligation_token_owner_pubkey: Pubkey, lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), + AccountMeta::new_readonly(deposit_reserve_pubkey, false), + AccountMeta::new(obligation_token_mint_pubkey, false), + AccountMeta::new(obligation_token_output_pubkey, false), + AccountMeta::new_readonly(obligation_token_owner_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } - .pack(), + data: LendingInstruction::InitObligationCollateral.pack(), } } -/// Creates a `LiquidateObligation` instruction +/// Creates a 'RefreshObligationCollateral' instruction. #[allow(clippy::too_many_arguments)] -pub fn liquidate_obligation( +pub fn refresh_obligation_collateral( program_id: Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, - source_liquidity_pubkey: Pubkey, - destination_collateral_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, - repay_reserve_liquidity_supply_pubkey: Pubkey, - withdraw_reserve_pubkey: Pubkey, - withdraw_reserve_collateral_supply_pubkey: Pubkey, - obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, obligation_collateral_pubkey: Pubkey, + deposit_reserve_pubkey: Pubkey, lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, + dex_market_pubkey: Pubkey, + dex_market_order_book_side_pubkey: Pubkey, + memory_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), - AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new(withdraw_reserve_pubkey, false), - AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new(obligation_collateral_pubkey, false), + AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(dex_market_pubkey, false), + AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), + AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::LiquidateObligation { - liquidity_amount, - liquidity_amount_type, - } - .pack(), - } -} - -/// Creates an `AccrueReserveInterest` instruction -pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { - let mut accounts = Vec::with_capacity(1 + reserve_pubkeys.len()); - accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)); - accounts.extend( - reserve_pubkeys - .into_iter() - .map(|reserve_pubkey| AccountMeta::new(reserve_pubkey, false)), - ); - Instruction { - program_id, - accounts, - data: LendingInstruction::AccrueReserveInterest.pack(), + data: LendingInstruction::RefreshObligationCollateral.pack(), } } @@ -996,55 +990,6 @@ pub fn withdraw_obligation_collateral( } } -/// Creates a 'SetLendingMarketOwner' instruction. -pub fn set_lending_market_owner( - program_id: Pubkey, - lending_market_pubkey: Pubkey, - lending_market_owner: Pubkey, - new_owner: Pubkey, -) -> Instruction { - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_owner, true), - ], - data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), - } -} - -/// Creates an 'InitObligationCollateral' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_obligation_collateral( - program_id: Pubkey, - obligation_pubkey: Pubkey, - obligation_collateral_pubkey: Pubkey, - deposit_reserve_pubkey: Pubkey, - obligation_token_mint_pubkey: Pubkey, - obligation_token_output_pubkey: Pubkey, - obligation_token_owner_pubkey: Pubkey, - lending_market_pubkey: Pubkey, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_collateral_pubkey, false), - AccountMeta::new_readonly(deposit_reserve_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), - AccountMeta::new_readonly(obligation_token_owner_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::InitObligationCollateral.pack(), - } -} - /// Creates an 'InitObligationLiquidity' instruction. #[allow(clippy::too_many_arguments)] pub fn init_obligation_liquidity( @@ -1061,6 +1006,7 @@ pub fn init_obligation_liquidity( AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], @@ -1068,12 +1014,12 @@ pub fn init_obligation_liquidity( } } -/// Creates a 'RefreshObligationCollateral' instruction. +/// Creates a 'RefreshObligationLiquidity' instruction. #[allow(clippy::too_many_arguments)] -pub fn refresh_obligation_collateral( +pub fn refresh_obligation_liquidity( program_id: Pubkey, - obligation_collateral_pubkey: Pubkey, - deposit_reserve_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + borrow_reserve_pubkey: Pubkey, lending_market_pubkey: Pubkey, dex_market_pubkey: Pubkey, dex_market_order_book_side_pubkey: Pubkey, @@ -1084,8 +1030,8 @@ pub fn refresh_obligation_collateral( Instruction { program_id, accounts: vec![ - AccountMeta::new(obligation_collateral_pubkey, false), - AccountMeta::new_readonly(deposit_reserve_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(dex_market_pubkey, false), @@ -1094,63 +1040,139 @@ pub fn refresh_obligation_collateral( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RefreshObligationCollateral.pack(), + data: LendingInstruction::RefreshObligationLiquidity.pack(), } } -/// Creates a 'RefreshObligationLiquidity' instruction. +/// Creates a 'BorrowObligationLiquidity' instruction. #[allow(clippy::too_many_arguments)] -pub fn refresh_obligation_liquidity( +pub fn borrow_obligation_liquidity( program_id: Pubkey, - obligation_liquidity_pubkey: Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, + borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, + obligation_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, lending_market_pubkey: Pubkey, dex_market_pubkey: Pubkey, dex_market_order_book_side_pubkey: Pubkey, memory_pubkey: Pubkey, + host_fee_receiver_pubkey: Option, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + let mut accounts = vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(borrow_reserve_pubkey, false), + AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(dex_market_pubkey, false), + AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), + AccountMeta::new_readonly(memory_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { + accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); + } + Instruction { + program_id, + accounts, + data: LendingInstruction::BorrowObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } + .pack(), + } +} + +/// Creates a `RepayObligationLiquidity` instruction +#[allow(clippy::too_many_arguments)] +pub fn repay_obligation_liquidity( + program_id: Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + obligation_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(obligation_pubkey, false), AccountMeta::new(obligation_liquidity_pubkey, false), - AccountMeta::new_readonly(borrow_reserve_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RefreshObligationLiquidity.pack(), + data: LendingInstruction::RepayObligationLiquidity { + liquidity_amount, + liquidity_amount_type, + } + .pack(), } } -/// Creates a 'RefreshObligation' instruction. +/// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] -pub fn refresh_obligation( +pub fn liquidate_obligation( program_id: Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + source_liquidity_pubkey: Pubkey, + destination_collateral_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + repay_reserve_liquidity_supply_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, + obligation_liquidity_pubkey: Pubkey, + obligation_collateral_pubkey: Pubkey, lending_market_pubkey: Pubkey, - obligation_collateral_liquidity_pubkeys: Vec, + user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let mut accounts = Vec::with_capacity(4 + obligation_collateral_liquidity_pubkeys.len()); - accounts.extend(vec![ - AccountMeta::new(obligation_pubkey, false) - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ]); - accounts.extend( - obligation_collateral_liquidity_pubkeys - .into_iter() - .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), - ); + let (lending_market_authority_pubkey, _bump_seed) = + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); Instruction { program_id, - accounts, - data: LendingInstruction::RefreshObligationLiquidity.pack(), + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(obligation_liquidity_pubkey, false), + AccountMeta::new(obligation_collateral_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::LiquidateObligation { + liquidity_amount, + liquidity_amount_type, + } + .pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 86e2da6735c..90373d2f2a4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -6,10 +6,11 @@ use crate::{ instruction::{init_lending_market, AmountType, LendingInstruction}, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, state::{ - BorrowResult, LendingMarket, LiquidateResult, NewObligationCollateralParams, - NewObligationLiquidityParams, NewObligationParams, NewReserveParams, Obligation, - ObligationCollateral, ObligationLiquidity, RepayResult, Reserve, ReserveCollateral, - ReserveConfig, ReserveLiquidity, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, + BorrowLiquidityResult, LendingMarket, LiquidateObligationResult, + NewObligationCollateralParams, NewObligationLiquidityParams, NewObligationParams, + NewReserveParams, Obligation, ObligationCollateral, ObligationLiquidity, + RepayLiquidityResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, + WithdrawCollateralResult, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, }, }; use num_traits::FromPrimitive; @@ -51,6 +52,10 @@ pub fn process_instruction( accounts, ) } + LendingInstruction::SetLendingMarketOwner { new_owner } => { + msg!("Instruction: Set Lending Market Owner"); + process_set_lending_market_owner(program_id, new_owner, accounts) + } LendingInstruction::InitReserve { liquidity_amount, config, @@ -58,23 +63,63 @@ pub fn process_instruction( msg!("Instruction: Init Reserve"); process_init_reserve(program_id, liquidity_amount, config, accounts) } + LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { + msg!("Instruction: Deposit Reserve Liquidity"); + process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts) + } + LendingInstruction::RedeemReserveCollateral { collateral_amount } => { + msg!("Instruction: Redeem Reserve Collateral"); + process_redeem_reserve_collateral(program_id, collateral_amount, accounts) + } + LendingInstruction::AccrueReserveInterest => { + msg!("Instruction: Accrue Reserve Interest"); + process_accrue_reserve_interest(program_id, accounts) + } LendingInstruction::InitObligation => { msg!("Instruction: Init Obligation"); process_init_obligation(program_id, accounts) } - LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { - msg!("Instruction: Deposit"); - process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts) + LendingInstruction::RefreshObligation => { + msg!("Instruction: Refresh Obligation"); + process_refresh_obligation(program_id, accounts) + } + LendingInstruction::InitObligationCollateral => { + msg!("Instruction: Init Obligation Collateral"); + process_init_obligation_collateral(program_id, accounts) + } + LendingInstruction::RefreshObligationCollateral => { + msg!("Instruction: Refresh Obligation Collateral"); + process_refresh_obligation_collateral(program_id, accounts) + } + LendingInstruction::DepositObligationCollateral { collateral_amount } => { + msg!("Instruction: Deposit Obligation Collateral"); + process_deposit_obligation_collateral(program_id, collateral_amount, accounts) + } + LendingInstruction::WithdrawObligationCollateral { + collateral_amount, + collateral_amount_type, + } => { + msg!("Instruction: Withdraw Obligation Collateral"); + process_withdraw_obligation_collateral( + program_id, + collateral_amount, + collateral_amount_type, + accounts, + ) } - LendingInstruction::WithdrawReserveLiquidity { collateral_amount } => { - msg!("Instruction: Withdraw"); - process_withdraw_reserve_liquidity(program_id, collateral_amount, accounts) + LendingInstruction::InitObligationLiquidity => { + msg!("Instruction: Init Obligation Liquidity"); + process_init_obligation_liquidity(program_id, accounts) + } + LendingInstruction::RefreshObligationLiquidity => { + msg!("Instruction: Refresh Obligation Liquidity"); + process_refresh_obligation_liquidity(program_id, accounts) } LendingInstruction::BorrowObligationLiquidity { liquidity_amount, liquidity_amount_type, } => { - msg!("Instruction: Borrow"); + msg!("Instruction: Borrow Obligation Liquidity"); process_borrow_obligation_liquidity( program_id, liquidity_amount, @@ -86,7 +131,7 @@ pub fn process_instruction( liquidity_amount, liquidity_amount_type, } => { - msg!("Instruction: Repay"); + msg!("Instruction: Repay Obligation Liquidity"); process_repay_obligation_liquidity( program_id, liquidity_amount, @@ -98,7 +143,7 @@ pub fn process_instruction( liquidity_amount, liquidity_amount_type, } => { - msg!("Instruction: Liquidate"); + msg!("Instruction: Liquidate Obligation"); process_liquidate_obligation( program_id, liquidity_amount, @@ -106,50 +151,6 @@ pub fn process_instruction( accounts, ) } - LendingInstruction::AccrueReserveInterest => { - msg!("Instruction: Accrue Interest"); - process_accrue_reserve_interest(program_id, accounts) - } - LendingInstruction::DepositObligationCollateral { collateral_amount } => { - msg!("Instruction: Deposit Obligation Collateral"); - process_deposit_obligation_collateral(program_id, collateral_amount, accounts) - } - LendingInstruction::WithdrawObligationCollateral { - collateral_amount, - collateral_amount_type, - } => { - msg!("Instruction: Withdraw Obligation Collateral"); - process_withdraw_obligation_collateral( - program_id, - collateral_amount, - collateral_amount_type, - accounts, - ) - } - LendingInstruction::SetLendingMarketOwner { new_owner } => { - msg!("Instruction: Set Lending Market Owner"); - process_set_lending_market_owner(program_id, new_owner, accounts) - } - LendingInstruction::InitObligationCollateral => { - msg!("Instruction: Init Obligation Collateral"); - process_init_obligation_collateral(program_id, accounts) - } - LendingInstruction::InitObligationLiquidity => { - msg!("Instruction: Init Obligation Liquidity"); - process_init_obligation_liquidity(program_id, accounts) - } - LendingInstruction::RefreshObligationCollateral => { - msg!("Instruction: Refresh Obligation Collateral"); - process_refresh_obligation_collateral(program_id, accounts) - } - LendingInstruction::RefreshObligationLiquidity => { - msg!("Instruction: Refresh Obligation Liquidity"); - process_refresh_obligation_liquidity(program_id, accounts) - } - LendingInstruction::RefreshObligation => { - msg!("Instruction: Refresh Obligation"); - process_refresh_obligation(program_id, accounts) - } } } @@ -197,6 +198,33 @@ fn process_init_lending_market( Ok(()) } +#[inline(never)] // avoid stack frame limit +fn process_set_lending_market_owner( + program_id: &Pubkey, + new_owner: Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_owner_info = next_account_info(account_info_iter)?; + + let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.owner != lending_market_owner_info.key { + return Err(LendingError::InvalidMarketOwner.into()); + } + if !lending_market_owner_info.is_signer { + return Err(LendingError::InvalidSigner.into()); + } + + lending_market.owner = new_owner; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; + + Ok(()) +} + fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, @@ -274,7 +302,7 @@ fn process_init_reserve( let dex_market = if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { let dex_market_info = next_account_info(account_info_iter)?; - // TODO: check that market state is owned by real serum dex program + // TODO: check that market state is owned by real serum dex program - https://git.io/JmwJ1 assert_rent_exempt(rent, dex_market_info)?; let dex_market_data = &dex_market_info.data.borrow(); @@ -307,21 +335,21 @@ fn process_init_reserve( return Err(LendingError::InvalidTokenOwner.into()); } - let reserve_liquidity_info = ReserveLiquidity::new( + let reserve_liquidity = ReserveLiquidity::new( *reserve_liquidity_mint_info.key, reserve_liquidity_mint.decimals, *reserve_liquidity_supply_info.key, *reserve_liquidity_fee_receiver_info.key, ); - let reserve_collateral_info = ReserveCollateral::new( + let reserve_collateral = ReserveCollateral::new( *reserve_collateral_mint_info.key, *reserve_collateral_supply_info.key, ); let mut reserve = Reserve::new(NewReserveParams { current_slot: clock.slot, lending_market: *lending_market_info.key, - liquidity: reserve_liquidity_info, - collateral: reserve_collateral_info, + liquidity: reserve_liquidity, + collateral: reserve_collateral, dex_market, config, }); @@ -389,35 +417,6 @@ fn process_init_reserve( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - - assert_rent_exempt(rent, obligation_info)?; - assert_uninitialized::(obligation_info)?; - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let obligation = Obligation::new(NewObligationParams { - lending_market: *lending_market_info.key, - collateral: vec![], - liquidity: vec![], - }); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - Ok(()) -} - fn process_deposit_reserve_liquidity( program_id: &Pubkey, liquidity_amount: u64, @@ -509,7 +508,7 @@ fn process_deposit_reserve_liquidity( Ok(()) } -fn process_withdraw_reserve_liquidity( +fn process_redeem_reserve_collateral( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], @@ -601,40 +600,33 @@ fn process_withdraw_reserve_liquidity( } #[inline(never)] // avoid stack frame limit -fn process_borrow_obligation_liquidity( - program_id: &Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, - accounts: &[AccountInfo], -) -> ProgramResult { - if liquidity_amount == 0 { - return Err(LendingError::InvalidAmount.into()); - } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); +fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + for reserve_info in account_info_iter { + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } + + reserve.accrue_interest(clock.slot)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; } + Ok(()) +} + +#[inline(never)] // avoid stack frame limit +fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } + assert_rent_exempt(rent, obligation_info)?; + assert_uninitialized::(obligation_info)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { @@ -644,37 +636,28 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { + let obligation = Obligation::new(NewObligationParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, + }); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + + Ok(()) +} + +fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &borrow_reserve.lending_market != lending_market_info.key { - return Err(LendingError::LendingMarketMismatch.into()); - } - if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Invalid source liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Invalid destination liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { - msg!("Invalid borrow reserve liquidity fee receiver account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if borrow_reserve.dex_market.is_none() { - msg!("Borrow reserve must have a dex market"); - return Err(LendingError::InvalidAccountInput.into()); - } - if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - } - if borrow_reserve.last_update.is_stale(clock.slot) { - return Err(LendingError::ReserveStale.into()); + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; @@ -685,174 +668,91 @@ fn process_borrow_obligation_liquidity( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationStale.into()); - } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update < borrow_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { - msg!("Invalid borrow reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - return Err(LendingError::ObligationAccountNotFound.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } + let mut collateral_value = Decimal::zero(); + for pubkey in obligation.collateral { + let obligation_collateral_info = next_account_info(account_info_iter)?; + if obligation_collateral_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if pubkey != obligation_collateral_info.key { + msg!("Invalid obligation collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } - // @TODO: is this necessary? - obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + let obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_collateral.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } - let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); - let loan_to_value = obligation.loan_to_value()?; - if loan_to_value > loan_to_value_ratio { - return Err(LendingError::ObligationLTVAboveReserveLTV.into()); - } - if loan_to_value == loan_to_value_ratio { - return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); + collateral_value = collateral_value.try_add(obligation_collateral.value)?; } - let token_converter = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - // @TODO: check these - &borrow_reserve.liquidity.mint_pubkey, - &lending_market.quote_token_mint, - )?; - - let BorrowResult { - total_amount, - borrow_amount, - origination_fee, - host_fee, - } = borrow_reserve.borrow_liquidity( - liquidity_amount, - liquidity_amount_type, - &obligation, - loan_to_value_ratio, - token_converter, - &lending_market.quote_token_mint, - )?; - - // @FIXME: fees come out of liquidity pool, but they don't get paid by borrower - // @FIXME: don't we need to adjust total supply? - borrow_reserve - .liquidity - .borrow(total_amount, borrow_amount)?; - // @TODO: will this need further adjustment for fees? - obligation_liquidity.borrow(borrow_amount); - obligation_liquidity.last_update.mark_stale(); - obligation.last_update.mark_stale(); - - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - - let mut owner_fee = origination_fee; - if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { - if host_fee > 0 { - owner_fee = owner_fee - .checked_sub(host_fee) - .ok_or(LendingError::MathOverflow)?; + let mut liquidity_value = Decimal::zero(); + for pubkey in obligation.liquidity { + let obligation_liquidity_info = next_account_info(account_info_iter)?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if pubkey != obligation_liquidity_info.key { + msg!("Invalid obligation liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: host_fee_receiver_info.clone(), - amount: host_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + let obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); } + + liquidity_value = liquidity_value.try_add(obligation_liquidity.value)?; } - if owner_fee > 0 { - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: borrow_reserve_liquidity_fee_receiver_info.clone(), - amount: owner_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + + // @TODO: check this + if account_info_iter.count() > 0 { + msg!("Too many obligation collateral or liquidity accounts"); + return Err(LendingError::InvalidAccountInput.into()); } - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: borrow_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + obligation.collateral_value = collateral_value; + obligation.liquidity_value = liquidity_value; + obligation.last_update.update_slot(clock.slot); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } #[inline(never)] // avoid stack frame limit -fn process_repay_obligation_liquidity( +fn process_init_obligation_collateral( program_id: &Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - return Err(LendingError::InvalidAmount.into()); - } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } - let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; + let obligation_token_mint_info = next_account_info(account_info_iter)?; + let obligation_token_output_info = next_account_info(account_info_iter)?; + let obligation_token_owner_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent_info = next_account_info(account_info_iter)?; + let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, obligation_collateral_info)?; + assert_uninitialized::(obligation_collateral_info)?; + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -861,25 +761,17 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.lending_market != lending_market_info.key { + if &deposit_reserve.lending_market != lending_market_info.key { msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - // @TODO: how is the currency/mint of the liquidity known? - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Invalid source liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { - msg!("Invalid destination liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if repay_reserve.last_update.is_stale(clock.slot) { - return Err(LendingError::ReserveStale.into()); + // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? + if deposit_reserve.config.loan_to_value_ratio == 0 { + return Err(LendingError::ReserveCollateralDisabled.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; @@ -890,39 +782,14 @@ fn process_repay_obligation_liquidity( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationStale.into()); - } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update < repay_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } - - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - return Err(LendingError::ObligationAccountNotFound.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); + if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { + return Err(LendingError::ObligationAccountLimit.into()); } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); + if obligation + .collateral + .contains(obligation_collateral_info.key) + { + return Err(LendingError::ObligationCollateralDuplicate.into()); } let authority_signer_seeds = &[ @@ -935,69 +802,52 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let RepayResult { - settle_amount, - repay_amount, - } = repay_reserve.repay_liquidity( - liquidity_amount, - liquidity_amount_type, - &obligation_liquidity, - ); - - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - obligation_liquidity.repay(settle_amount); - obligation_liquidity.last_update.mark_stale(); + let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { + current_slot: clock.slot, + obligation: *obligation_info.key, + deposit_reserve: *deposit_reserve_info.key, + token_mint: obligation_token_mint_info.key(), + }); + obligation.collateral.push(*obligation_collateral_info.key); obligation.last_update.mark_stale(); - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: repay_reserve_liquidity_supply_info.clone(), - amount: repay_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], + spl_token_init_mint(TokenInitializeMintParams { + mint: obligation_token_mint_info.clone(), + authority: lending_market_authority_info.key, + rent: rent_info.clone(), + decimals: deposit_reserve.liquidity.mint_decimals, + token_program: token_program_id.clone(), + })?; + + spl_token_init_account(TokenInitializeAccountParams { + account: obligation_token_output_info.clone(), + mint: obligation_token_mint_info.clone(), + owner: obligation_token_owner_info.clone(), + rent: rent_info.clone(), token_program: token_program_id.clone(), })?; Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_liquidate_obligation( +fn process_refresh_obligation_collateral( program_id: &Pubkey, - liquidity_amount: u64, - liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - return Err(LendingError::InvalidAmount.into()); - } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } - let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let withdraw_reserve_info = next_account_info(account_info_iter)?; - let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; - let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; let obligation_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; + let dex_market_info = next_account_info(account_info_iter)?; + let dex_market_orders_info = next_account_info(account_info_iter)?; + let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1013,140 +863,37 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidTokenProgram.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.lending_market != lending_market_info.key { + if &deposit_reserve.lending_market != lending_market_info.key { msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Invalid repay reserve liquidity supply account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Invalid source liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if repay_reserve.last_update.is_stale(clock.slot) { - return Err(LendingError::ReserveStale.into()); - } - - let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; - if withdraw_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if withdraw_reserve.lending_market != repay_reserve.lending_market { - return Err(LendingError::LendingMarketMismatch.into()); - } - if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Invalid withdraw reserve collateral supply account"); + if deposit_reserve.dex_market.is_none() { + msg!("Deposit reserve must have a dex market"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Invalid destination collateral account"); - return Err(LendingError::InvalidAccountInput.into()); + if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { + if &dex_market_pubkey != dex_market_info.key { + msg!("Invalid dex market account"); + return Err(LendingError::InvalidAccountInput.into()); + } } - if withdraw_reserve.last_update.is_stale(clock.slot) { + if deposit_reserve.last_update.is_stale(clock.slot) { return Err(LendingError::ReserveStale.into()); } - // @TODO: are these two checks necessary? - // what's the problem with repaying and receiving the same currency? - if repay_reserve_info.key == withdraw_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if repay_reserve.liquidity.mint_pubkey == withdraw_reserve.liquidity.mint_pubkey { - return Err(LendingError::DuplicateReserveMint.into()); - } - - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationStale.into()); - } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued - if obligation.last_update < repay_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation.last_update < withdraw_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } - - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - return Err(LendingError::ObligationAccountNotFound.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation_liquidity.borrowed_wads == 0 { - // @TODO: make specific to liquidity - return Err(LendingError::ObligationEmpty.into()); - } - let mut obligation_collateral = ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; if obligation_collateral_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); + if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { + msg!("Invalid deposit reserve account"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); - } - if !obligation - .collateral - .contains(obligation_collateral_info.key) - { - return Err(LendingError::ObligationAccountNotFound.into()); - } - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_collateral.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation_collateral.deposited_tokens == 0 { - // @TODO: make specific to collateral - return Err(LendingError::ObligationEmpty.into()); - } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1158,81 +905,28 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); - let obligation_ltv = obligation.loan_to_value()?; - if obligation_ltv < liquidation_threshold { - return Err(LendingError::HealthyObligation.into()); - } - - let LiquidateResult { - settle_amount, - repay_amount, - withdraw_amount, - } = withdraw_reserve.liquidate_obligation( - liquidity_amount, - liquidity_amount_type, - &obligation, - &obligation_liquidity, - &obligation_collateral, + let token_converter = TradeSimulator::new( + dex_market_info, + dex_market_orders_info, + memory, + &lending_market.quote_token_mint, + // @TODO: check these + &lending_market.quote_token_mint, + &deposit_reserve.liquidity.mint_pubkey, )?; - if withdraw_amount == 0 { - return Err(LendingError::LiquidationTooSmall.into()); - } - - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - obligation_liquidity.repay(settle_amount); - // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? - obligation_collateral.withdraw(withdraw_amount); - obligation_liquidity.last_update.mark_stale(); - obligation_collateral.last_update.mark_stale(); - obligation.last_update.mark_stale(); - - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + obligation_collateral.update_value( + deposit_reserve.collateral_exchange_rate()?, + token_converter, + &deposit_reserve.liquidity.mint_pubkey, + )?; + obligation_collateral.last_update.update_slot(clock.slot); ObligationCollateral::pack( obligation_collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: repay_reserve_liquidity_supply_info.clone(), - amount: repay_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; - - spl_token_transfer(TokenTransferParams { - source: withdraw_reserve_collateral_supply_info.clone(), - destination: destination_collateral_info.clone(), - amount: withdraw_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - - Ok(()) -} - -#[inline(never)] // avoid stack frame limit -fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - for reserve_info in account_info_iter { - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - - reserve.accrue_interest(clock.slot)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; - } + // @TODO: should we mark the obligation stale here? + // could also iteratively update obligation.collateral_value Ok(()) } @@ -1276,10 +970,6 @@ fn process_deposit_obligation_collateral( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? - if deposit_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); @@ -1288,6 +978,10 @@ fn process_deposit_obligation_collateral( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? + if deposit_reserve.config.loan_to_value_ratio == 0 { + return Err(LendingError::ReserveCollateralDisabled.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -1319,7 +1013,8 @@ fn process_deposit_obligation_collateral( .collateral .contains(obligation_collateral_info.key) { - return Err(LendingError::ObligationAccountNotFound.into()); + msg!("Invalid obligation collateral account"); + return Err(LendingError::InvalidAccountInput.into()); } unpack_mint(&obligation_token_mint_info.data.borrow())?; @@ -1470,20 +1165,20 @@ fn process_withdraw_obligation_collateral( .collateral .contains(obligation_collateral_info.key) { - return Err(LendingError::ObligationAccountNotFound.into()); + msg!("Invalid obligation collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_collateral.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); } // @TODO: is this enough? other collateral/liquidity could have been updated that we don't // check here. we could mark the obligation stale on every refresh of // collateral/liquidity, but this means they can't be refreshed in parallel if obligation.last_update < obligation_collateral.last_update { - return Err(LendingError::ObligationCollateralStale.into()); - } - // @TODO: is this necessary if checking obligation.last_update < obligation_liquidity.last_update above? - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); + return Err(LendingError::ObligationStale.into()); } if obligation_collateral.deposited_tokens == 0 { - return Err(LendingError::ObligationEmpty.into()); + return Err(LendingError::ObligationCollateralEmpty.into()); } let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; @@ -1509,46 +1204,26 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let lending_market_ltv = Rate::from_percent(lending_market.loan_to_value_ratio); - let obligation_ltv = obligation.loan_to_value()?; - if obligation_ltv > lending_market_ltv { - return Err(LendingError::ObligationLTVAboveReserveLTV.into()); + // @TODO: consider moving to Obligation::withdraw_collateral + let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); + let loan_to_value = obligation.loan_to_value()?; + if loan_to_value > loan_to_value_ratio { + return Err(LendingError::ObligationLTVAboveLendingMarketLTV.into()); } - if obligation_ltv == lending_market_ltv { - return Err(LendingError::ObligationLTVCannotGoAboveReserveLTV.into()); + if loan_to_value == loan_to_value_ratio { + return Err(LendingError::ObligationLTVCannotGoAboveLendingMarketLTV.into()); } - let min_collateral_value = obligation.liquidity_value.try_div(lending_market_ltv)?; - let max_withdraw_value = obligation.collateral_value.try_sub(min_collateral_value)?; - - let withdraw_amount = match collateral_amount_type { - AmountType::ExactAmount => { - let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_tokens); - let withdraw_pct = - Decimal::from(withdraw_amount).try_div(obligation_collateral.deposited_tokens)?; - let withdraw_value = obligation.collateral_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - return Err(LendingError::ObligationCollateralWithdrawBelowRequired.into()); - } - - withdraw_amount - } - AmountType::PercentAmount => { - let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); - let withdraw_value = max_withdraw_value - .try_mul(withdraw_pct)? - .min(obligation_collateral.value); - let withdraw_amount = withdraw_value - .try_div(obligation_collateral.value)? - .try_mul(obligation_collateral.deposited_tokens)? - .try_floor_u64()?; - - withdraw_amount - } - }; - - let obligation_token_amount = obligation_collateral - .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; + let WithdrawCollateralResult { + withdraw_amount, + obligation_token_amount, + } = obligation.withdraw_collateral( + collateral_amount, + collateral_amount_type, + &obligation_collateral, + loan_to_value_ratio, + obligation_token_mint.supply, + )?; // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? obligation_collateral.withdraw(withdraw_amount)?; @@ -1582,133 +1257,6 @@ fn process_withdraw_obligation_collateral( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_set_lending_market_owner( - program_id: &Pubkey, - new_owner: Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_owner_info = next_account_info(account_info_iter)?; - - let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.owner != lending_market_owner_info.key { - return Err(LendingError::InvalidMarketOwner.into()); - } - if !lending_market_owner_info.is_signer { - return Err(LendingError::InvalidSigner.into()); - } - - lending_market.owner = new_owner; - LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; - - Ok(()) -} - -#[inline(never)] // avoid stack frame limit -fn process_init_obligation_collateral( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; - let obligation_token_owner_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; - let token_program_id = next_account_info(account_info_iter)?; - - assert_rent_exempt(rent, obligation_collateral_info)?; - assert_uninitialized::(obligation_collateral_info)?; - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? - if deposit_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } - - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { - return Err(LendingError::ObligationAccountLimit.into()); - } - if obligation.collateral.contains(deposit_reserve_info.key) { - return Err(LendingError::ObligationAccountDuplicate.into()); - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { - obligation: *obligation_info.key, - deposit_reserve: *deposit_reserve_info.key, - token_mint: obligation_token_mint_info.key(), - }); - obligation.collateral.push(*obligation_collateral_info.key); - obligation.last_update.mark_stale(); - - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - spl_token_init_mint(TokenInitializeMintParams { - mint: obligation_token_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: deposit_reserve.liquidity.mint_decimals, - token_program: token_program_id.clone(), - })?; - - spl_token_init_account(TokenInitializeAccountParams { - account: obligation_token_output_info.clone(), - mint: obligation_token_mint_info.clone(), - owner: obligation_token_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - - Ok(()) -} - #[inline(never)] // avoid stack frame limit fn process_init_obligation_liquidity( program_id: &Pubkey, @@ -1719,6 +1267,7 @@ fn process_init_obligation_liquidity( let obligation_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1754,11 +1303,12 @@ fn process_init_obligation_liquidity( if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { return Err(LendingError::ObligationAccountLimit.into()); } - if obligation.liquidity.contains(borrow_reserve_info.key) { - return Err(LendingError::ObligationAccountDuplicate.into()); + if obligation.liquidity.contains(obligation_liquidity_info.key) { + return Err(LendingError::ObligationLiquidityDuplicate.into()); } let obligation_liquidity = ObligationLiquidity::new(NewObligationLiquidityParams { + current_slot: clock.slot, obligation: *obligation_info.key, borrow_reserve: *borrow_reserve_info.key, }); @@ -1774,13 +1324,13 @@ fn process_init_obligation_liquidity( Ok(()) } -fn process_refresh_obligation_collateral( +fn process_refresh_obligation_liquidity( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let obligation_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; + let borrow_reserve_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let dex_market_info = next_account_info(account_info_iter)?; @@ -1801,35 +1351,37 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidTokenProgram.into()); } - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &deposit_reserve.lending_market != lending_market_info.key { + if &borrow_reserve.lending_market != lending_market_info.key { msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if deposit_reserve.dex_market.is_none() { - msg!("Deposit reserve must have a dex market"); + if borrow_reserve.dex_market.is_none() { + msg!("Borrow reserve must have a dex market"); return Err(LendingError::InvalidAccountInput.into()); } - if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { + if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { if &dex_market_pubkey != dex_market_info.key { msg!("Invalid dex market account"); return Err(LendingError::InvalidAccountInput.into()); } } - if deposit_reserve.last_update.is_stale(clock.slot) { + // @TODO: is this enough? borrow_reserve.cumulative_borrow_rate_wads is used below + // might need to restrict to current slot + if borrow_reserve.last_update.is_stale(clock.slot) { return Err(LendingError::ReserveStale.into()); } - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { - msg!("Invalid deposit reserve account"); + if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { + msg!("Invalid borrow reserve account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -1850,32 +1402,46 @@ fn process_refresh_obligation_collateral( &lending_market.quote_token_mint, // @TODO: check these &lending_market.quote_token_mint, - &deposit_reserve.liquidity.mint_pubkey, + &borrow_reserve.liquidity.mint_pubkey, )?; - obligation_collateral.update_value( - deposit_reserve.collateral_exchange_rate()?, - token_converter, - &deposit_reserve.liquidity.mint_pubkey, - )?; - obligation_collateral.update_slot(clock.slot)?; - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), + obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + obligation_liquidity.update_value(token_converter, &borrow_reserve.liquidity.mint_pubkey)?; + obligation_liquidity.last_update.update_slot(clock.slot); + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), )?; - // @TODO: should we mark the obligation stale here? could also iteratively update - // obligation.collateral_value + // @TODO: should we mark the obligation stale here? + // could also iteratively update obligation.liquidity_value Ok(()) } -fn process_refresh_obligation_liquidity( +#[inline(never)] // avoid stack frame limit +fn process_borrow_obligation_liquidity( program_id: &Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { + if liquidity_amount == 0 { + return Err(LendingError::InvalidAmount.into()); + } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } + let account_info_iter = &mut accounts.iter(); - let obligation_liquidity_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; + let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let obligation_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let dex_market_info = next_account_info(account_info_iter)?; @@ -1896,7 +1462,7 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } @@ -1904,6 +1470,18 @@ fn process_refresh_obligation_liquidity( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { + msg!("Invalid source liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + msg!("Invalid destination liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { + msg!("Invalid borrow reserve liquidity fee receiver account"); + return Err(LendingError::InvalidAccountInput.into()); + } if borrow_reserve.dex_market.is_none() { msg!("Borrow reserve must have a dex market"); return Err(LendingError::InvalidAccountInput.into()); @@ -1918,24 +1496,70 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::ReserveStale.into()); } - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { - msg!("Invalid borrow reserve account"); + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); + if obligation.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update < borrow_reserve.last_update { + return Err(LendingError::ObligationStale.into()); + } + + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { + msg!("Invalid borrow reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + msg!("Invalid obligation liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update < obligation_liquidity.last_update { + return Err(LendingError::ObligationStale.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + // @TODO: is this necessary? + obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + + let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); + let loan_to_value = obligation.loan_to_value()?; + if loan_to_value > loan_to_value_ratio { + return Err(LendingError::ObligationLTVAboveLendingMarketLTV.into()); + } + if loan_to_value == loan_to_value_ratio { + return Err(LendingError::ObligationLTVCannotGoAboveLendingMarketLTV.into()); } let token_converter = TradeSimulator::new( @@ -1944,27 +1568,108 @@ fn process_refresh_obligation_liquidity( memory, &lending_market.quote_token_mint, // @TODO: check these - &lending_market.quote_token_mint, &borrow_reserve.liquidity.mint_pubkey, + &lending_market.quote_token_mint, )?; - obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; - obligation_liquidity.update_value(token_converter, &borrow_reserve.liquidity.mint_pubkey)?; - obligation_liquidity.update_slot(clock.slot)?; + let BorrowLiquidityResult { + total_amount, + borrow_amount, + origination_fee, + host_fee, + } = borrow_reserve.borrow_liquidity( + liquidity_amount, + liquidity_amount_type, + &obligation, + loan_to_value_ratio, + token_converter, + &lending_market.quote_token_mint, + )?; + + // @FIXME: fees come out of liquidity pool, but they don't get paid by borrower + // @FIXME: don't we need to adjust total supply? + borrow_reserve + .liquidity + .borrow(total_amount, borrow_amount)?; + // @TODO: will this need further adjustment for fees? + obligation_liquidity.borrow(borrow_amount); + obligation_liquidity.last_update.mark_stale(); + obligation.last_update.mark_stale(); + ObligationLiquidity::pack( obligation_liquidity, &mut obligation_liquidity_info.data.borrow_mut(), )?; - // @TODO: should we mark the obligation stale here? could also iteratively update - // obligation.liquidity_value + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; + + let mut owner_fee = origination_fee; + if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { + if host_fee > 0 { + owner_fee = owner_fee + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: host_fee_receiver_info.clone(), + amount: host_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + } + } + if owner_fee > 0 { + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: borrow_reserve_liquidity_fee_receiver_info.clone(), + amount: owner_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + } + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: borrow_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; Ok(()) } -fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { +#[inline(never)] // avoid stack frame limit +fn process_repay_obligation_liquidity( + program_id: &Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + accounts: &[AccountInfo], +) -> ProgramResult { + if liquidity_amount == 0 { + return Err(LendingError::InvalidAmount.into()); + } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); + } + } + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; + let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1976,6 +1681,27 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidTokenProgram.into()); } + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Invalid reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + // @TODO: how is the currency/mint of the liquidity known? + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Invalid source liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { + msg!("Invalid destination liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if repay_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1984,66 +1710,336 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if obligation.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update < repay_reserve.last_update { + return Err(LendingError::ObligationStale.into()); + } - let mut collateral_value = Decimal::zero(); - for pubkey in obligation.collateral { - let obligation_collateral_info = next_account_info(account_info_iter)?; - if obligation_collateral_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if pubkey != obligation_collateral_info.key { - msg!("Invalid obligation collateral account"); - return Err(LendingError::InvalidAccountInput.into()); - } + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { + msg!("Invalid repay reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + msg!("Invalid obligation liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update < obligation_liquidity.last_update { + return Err(LendingError::ObligationStale.into()); + } - let obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + let RepayLiquidityResult { + settle_amount, + repay_amount, + } = repay_reserve.repay_liquidity( + liquidity_amount, + liquidity_amount_type, + &obligation_liquidity, + ); + + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + obligation_liquidity.repay(settle_amount); + obligation_liquidity.last_update.mark_stale(); + obligation.last_update.mark_stale(); + + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: repay_reserve_liquidity_supply_info.clone(), + amount: repay_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + + Ok(()) +} + +#[inline(never)] // avoid stack frame limit +fn process_liquidate_obligation( + program_id: &Pubkey, + liquidity_amount: u64, + liquidity_amount_type: AmountType, + accounts: &[AccountInfo], +) -> ProgramResult { + if liquidity_amount == 0 { + return Err(LendingError::InvalidAmount.into()); + } + if let AmountType::PercentAmount = liquidity_amount_type { + if liquidity_amount > 100 { + msg!("Liquidity amount must be in range (0, 100]"); + return Err(LendingError::InvalidAmount.into()); } + } - collateral_value = collateral_value.try_add(obligation_collateral.value)?; + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; + let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let withdraw_reserve_info = next_account_info(account_info_iter)?; + let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; + let obligation_info = next_account_info(account_info_iter)?; + let obligation_liquidity_info = next_account_info(account_info_iter)?; + let obligation_collateral_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; + + if memory.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); } - let mut liquidity_value = Decimal::zero(); - for pubkey in obligation.liquidity { - let obligation_liquidity_info = next_account_info(account_info_iter)?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if pubkey != obligation_liquidity_info.key { - msg!("Invalid obligation liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + return Err(LendingError::InvalidTokenProgram.into()); + } - let obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Invalid repay reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { + msg!("Invalid repay reserve liquidity supply account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Invalid source liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if repay_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } - liquidity_value = liquidity_value.try_add(obligation_liquidity.value)?; + let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; + if withdraw_reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &withdraw_reserve.lending_market != lending_market_info.key { + msg!("Invalid withdraw reserve lending market account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { + msg!("Invalid withdraw reserve collateral supply account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!("Invalid destination collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if withdraw_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); } - // @TODO: check this - if account_info_iter.count() > 0 { - msg!("Too many obligation collateral or liquidity accounts"); + // @TODO: are these two checks necessary? + // is there a problem with repaying and receiving the same currency? + if repay_reserve_info.key == withdraw_reserve_info.key { + return Err(LendingError::DuplicateReserve.into()); + } + if repay_reserve.liquidity.mint_pubkey == withdraw_reserve.liquidity.mint_pubkey { + return Err(LendingError::DuplicateReserveMint.into()); + } + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation.lending_market != lending_market_info.key { + msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if obligation.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationStale.into()); + } + // @TODO: is this enough? other reserves could have been updated that we don't check here, and + // they all affect the market value. need to think about when interest may be accrued + if obligation.last_update < repay_reserve.last_update { + return Err(LendingError::ObligationStale.into()); + } + if obligation.last_update < withdraw_reserve.last_update { + return Err(LendingError::ObligationStale.into()); + } - obligation.collateral_value = collateral_value; - obligation.liquidity_value = liquidity_value; - obligation.update_slot(clock.slot)?; + let mut obligation_liquidity = + ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; + if obligation_liquidity_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_liquidity.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { + msg!("Invalid repay reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if !obligation.liquidity.contains(obligation_liquidity_info.key) { + msg!("Invalid obligation liquidity account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_liquidity.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationLiquidityStale.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update < obligation_liquidity.last_update { + return Err(LendingError::ObligationStale.into()); + } + if obligation_liquidity.borrowed_wads == Decimal::zero() { + return Err(LendingError::ObligationLiquidityEmpty.into()); + } + + let mut obligation_collateral = + ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; + if obligation_collateral_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &obligation_collateral.obligation != obligation_info.key { + msg!("Invalid obligation account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { + msg!("Invalid withdraw reserve account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &obligation_collateral.token_mint != obligation_token_mint_info.key { + msg!("Invalid obligation token mint"); + return Err(LendingError::InvalidTokenMint.into()); + } + if !obligation + .collateral + .contains(obligation_collateral_info.key) + { + msg!("Invalid obligation collateral account"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation_collateral.last_update.is_stale(clock.slot)? { + return Err(LendingError::ObligationCollateralStale.into()); + } + // @TODO: is this enough? other collateral/liquidity could have been updated that we don't + // check here. we could mark the obligation stale on every refresh of + // collateral/liquidity, but this means they can't be refreshed in parallel + if obligation.last_update < obligation_collateral.last_update { + return Err(LendingError::ObligationStale.into()); + } + if obligation_collateral.deposited_tokens == 0 { + return Err(LendingError::ObligationCollateralEmpty.into()); + } + + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if lending_market_authority_info.key != &lending_market_authority_pubkey { + return Err(LendingError::InvalidMarketAuthority.into()); + } + + let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); + let obligation_ltv = obligation.loan_to_value()?; + if obligation_ltv < liquidation_threshold { + return Err(LendingError::ObligationHealthy.into()); + } + + let LiquidateObligationResult { + settle_amount, + repay_amount, + withdraw_amount, + } = withdraw_reserve.liquidate_obligation( + liquidity_amount, + liquidity_amount_type, + &obligation, + &obligation_liquidity, + &obligation_collateral, + )?; + + // @TODO: check if this can actually happen now + if withdraw_amount == 0 { + return Err(LendingError::LiquidationTooSmall.into()); + } + + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + obligation_liquidity.repay(settle_amount); + // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? + obligation_collateral.withdraw(withdraw_amount); + obligation_liquidity.last_update.mark_stale(); + obligation_collateral.last_update.mark_stale(); + obligation.last_update.mark_stale(); + + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + ObligationCollateral::pack( + obligation_collateral, + &mut obligation_collateral_info.data.borrow_mut(), + )?; + ObligationLiquidity::pack( + obligation_liquidity, + &mut obligation_liquidity_info.data.borrow_mut(), + )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: repay_reserve_liquidity_supply_info.clone(), + amount: repay_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + + spl_token_transfer(TokenTransferParams { + source: withdraw_reserve_collateral_supply_info.clone(), + destination: destination_collateral_info.clone(), + amount: withdraw_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + Ok(()) } diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index a479c67edc1..49c51f4887e 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -19,17 +19,18 @@ pub const STALE_AFTER_SLOTS: u64 = 10; 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() -> Self { - Self { slot: 0 } + pub fn new(slot: Slot) -> Self { + Self { slot, stale: true } } /// Return slots elapsed since given slot pub fn slots_elapsed(&self, slot: Slot) -> Result { - // @FIXME: what should happen if self.slot == 0? let slots_elapsed = slot .checked_sub(self.slot) .ok_or(LendingError::MathOverflow)?; @@ -39,17 +40,17 @@ impl LastUpdate { /// Set last update slot pub fn update_slot(&mut self, slot: Slot) { self.slot = slot; + self.stale = false; } - // @FIXME: this will screw up interest rate tracking - /// Set last update slot to 0 + /// Set stale to true pub fn mark_stale(&mut self) { - self.update_slot(0); + self.stale = true; } /// Check if last update slot is too long ago pub fn is_stale(&self, slot: Slot) -> Result { - Ok(self.slot == 0 || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) + Ok(self.stale || self.slots_elapsed(slot)? > STALE_AFTER_SLOTS) } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index f91678859b2..40ad9f80637 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -1,6 +1,7 @@ use super::*; use crate::{ error::LendingError, + instruction::AmountType, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; @@ -22,7 +23,7 @@ pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; pub struct Obligation { /// Version of the struct pub version: u8, - /// Last slot when loan to value updated; set to 0 if collateral or liquidity changed + /// Last update to collateral, liquidity, or their market values pub last_update: LastUpdate, /// Lending market address pub lending_market: Pubkey, @@ -38,38 +39,91 @@ pub struct Obligation { /// Create new obligation pub struct NewObligationParams { + /// Current slot + pub current_slot: Slot, /// Lending market address pub lending_market: Pubkey, - /// Collateral for the obligation - pub collateral: Vec, - /// Liquidity for the obligation - pub liquidity: Vec, } impl Obligation { /// Create new obligation pub fn new(params: NewObligationParams) -> Self { let NewObligationParams { + current_slot, lending_market, - collateral, - liquidity, } = params; Self { version: PROGRAM_VERSION, - last_update: LastUpdate::new(), + last_update: LastUpdate::new(current_slot), lending_market, collateral_value: Decimal::zero(), liquidity_value: Decimal::zero(), - collateral, - liquidity, + collateral: vec![], + liquidity: vec![], } } + // @FIXME: error if collateral value is zero /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { self.liquidity_value.try_div(self.collateral_value) } + + pub fn withdraw_collateral( + &self, + collateral_amount: u64, + collateral_amount_type: AmountType, + obligation_collateral: &ObligationCollateral, + loan_to_value_ratio: Rate, + obligation_token_supply: u64, + ) -> Result { + let min_collateral_value = self.liquidity_value.try_div(loan_to_value_ratio)?; + let max_withdraw_value = self.collateral_value.try_sub(min_collateral_value)?; + + let withdraw_amount = match collateral_amount_type { + AmountType::ExactAmount => { + let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_tokens); + let withdraw_pct = Decimal::from(withdraw_amount) + .try_div(obligation_collateral.deposited_tokens)?; + let withdraw_value = self.collateral_value.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + return Err(LendingError::ObligationCollateralWithdrawTooLarge.into()); + } + + withdraw_amount + } + AmountType::PercentAmount => { + let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); + let withdraw_value = max_withdraw_value + .try_mul(withdraw_pct)? + .min(obligation_collateral.value); + let withdraw_amount = withdraw_value + .try_div(obligation_collateral.value)? + .try_mul(obligation_collateral.deposited_tokens)? + .try_floor_u64()?; + + withdraw_amount + } + }; + + let obligation_token_amount = obligation_collateral + .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_supply)?; + + Ok(WithdrawCollateralResult { + withdraw_amount, + obligation_token_amount, + }) + } +} + +/// Withdraw collateral result +#[derive(Debug)] +pub struct WithdrawCollateralResult { + /// Collateral tokens to withdraw + withdraw_amount: u64, + /// Obligation tokens to burn + obligation_token_amount: u64, } impl Sealed for Obligation {} @@ -79,7 +133,7 @@ impl IsInitialized for Obligation { } } -const OBLIGATION_LEN: usize = 395; // 1 + 8 + 32 + 16 + 16 + 1 + 1 + (32 * 10) +const OBLIGATION_LEN: usize = 716; // 1 + 8 + 1 + 32 + 16 + 16 + 1 + 1 + (32 * 10) + (32 * 10) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -89,26 +143,31 @@ impl Pack for Obligation { let ( version, last_update_slot, + last_update_stale, lending_market, collateral_value, liquidity_value, num_collateral, num_liquidity, accounts_flat, + _padding, ) = mut_array_refs![ output, 1, 8, + 1, PUBKEY_LEN, 16, 16, 1, 1, + PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); + *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); pack_decimal(self.collateral_value, collateral_value); pack_decimal(self.liquidity_value, liquidity_value); @@ -138,21 +197,25 @@ impl Pack for Obligation { let ( version, last_update_slot, + last_update_stale, lending_market, collateral_value, liquidity_value, num_collateral, num_liquidity, accounts_flat, + _padding, ) = array_refs![ input, 1, 8, + 1, PUBKEY_LEN, 16, 16, 1, 1, + PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS, PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS ]; @@ -183,6 +246,7 @@ impl Pack for Obligation { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), + stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, lending_market: Pubkey::new_from_array(*lending_market), collateral_value: unpack_decimal(collateral_value), diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index ba109cddb90..11e5f276ac4 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -18,7 +18,7 @@ use std::convert::TryInto; pub struct ObligationCollateral { /// Version of the obligation collateral pub version: u8, - /// Last slot when market value updated; set to 0 if deposited tokens changed + /// Last update to deposited tokens or their market value pub last_update: LastUpdate, /// Obligation the collateral is associated with pub obligation: Pubkey, @@ -34,6 +34,8 @@ pub struct ObligationCollateral { /// Create new obligation collateral pub struct NewObligationCollateralParams { + /// Current slot + pub current_slot: Slot, /// Obligation address pub obligation: Pubkey, /// Deposit reserve address @@ -46,6 +48,7 @@ impl ObligationCollateral { /// Create new obligation collateral pub fn new(params: NewObligationCollateralParams) -> Self { let NewObligationCollateralParams { + current_slot, obligation, deposit_reserve, token_mint, @@ -53,7 +56,7 @@ impl ObligationCollateral { Self { version: PROGRAM_VERSION, - last_update: LastUpdate::new(), + last_update: LastUpdate::new(current_slot), obligation, deposit_reserve, token_mint, @@ -114,7 +117,7 @@ impl IsInitialized for ObligationCollateral { } } -const OBLIGATION_COLLATERAL_LEN: usize = 257; // 1 + 32 + 32 + 32 + 8 + 8 + 16 + 128 +const OBLIGATION_COLLATERAL_LEN: usize = 258; // 1 + 8 + 1 + 32 + 32 + 32 + 8 + 16 + 128 impl Pack for ObligationCollateral { const LEN: usize = OBLIGATION_COLLATERAL_LEN; @@ -123,16 +126,18 @@ impl Pack for ObligationCollateral { let ( version, last_update_slot, + last_update_stale, obligation, deposit_reserve, token_mint, deposited_tokens, value, _padding, - ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; + ) = mut_array_refs![output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); + *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); obligation.copy_from_slice(self.obligation.as_ref()); deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); token_mint.copy_from_slice(self.token_mint.as_ref()); @@ -147,18 +152,20 @@ impl Pack for ObligationCollateral { let ( version, last_update_slot, + last_update_stale, obligation, deposit_reserve, token_mint, deposited_tokens, value, _padding, - ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; + ) = array_refs![input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), + stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, obligation: Pubkey::new_from_array(*obligation), deposit_reserve: Pubkey::new_from_array(*deposit_reserve), diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 2243e4f74ca..5b271e247b4 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -18,7 +18,7 @@ use std::convert::TryInto; pub struct ObligationLiquidity { /// Version of the obligation liquidity pub version: u8, - /// Last slot when market value and accrued interest updated; set to 0 if borrowed wads changed + /// Last update to accrued interest, borrowed wads, or their market value pub last_update: LastUpdate, /// Obligation the liquidity is associated with pub obligation: Pubkey, @@ -34,6 +34,8 @@ pub struct ObligationLiquidity { /// Create new obligation liquidity pub struct NewObligationLiquidityParams { + /// Current slot + pub current_slot: Slot, /// Obligation address pub obligation: Pubkey, /// Borrow reserve address @@ -44,13 +46,14 @@ impl ObligationLiquidity { /// Create new obligation liquidity pub fn new(params: NewObligationLiquidityParams) -> Self { let NewObligationLiquidityParams { + current_slot, obligation, borrow_reserve, } = params; Self { version: PROGRAM_VERSION, - last_update: LastUpdate::new(), + last_update: LastUpdate::new(current_slot), obligation, borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), @@ -106,7 +109,7 @@ impl IsInitialized for ObligationLiquidity { } } -const OBLIGATION_LIQUIDITY_LEN: usize = 249; // 1 + 8 + 32 + 32 + 16 + 16 + 16 + 128 +const OBLIGATION_LIQUIDITY_LEN: usize = 250; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 128 impl Pack for ObligationLiquidity { const LEN: usize = OBLIGATION_LIQUIDITY_LEN; @@ -115,16 +118,18 @@ impl Pack for ObligationLiquidity { let ( version, last_update_slot, + last_update_stale, obligation, borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, value, _padding, - ) = mut_array_refs![output, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; + ) = mut_array_refs![output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); + *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); obligation.copy_from_slice(self.obligation.as_ref()); borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); pack_decimal( @@ -142,18 +147,20 @@ impl Pack for ObligationLiquidity { let ( version, last_update_slot, + last_update_stale, obligation, borrow_reserve, cumulative_borrow_rate_wads, borrowed_wads, value, _padding, - ) = array_refs![input, 1, 8, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; + ) = array_refs![input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), + stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, obligation: Pubkey::new_from_array(*obligation), borrow_reserve: Pubkey::new_from_array(*borrow_reserve), diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 92e3e7baeb6..7df16fe3ae6 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -34,9 +34,9 @@ pub struct Reserve { pub lending_market: Pubkey, /// Dex market state account pub dex_market: COption, - /// Reserve liquidity info + /// Reserve liquidity pub liquidity: ReserveLiquidity, - /// Reserve collateral info + /// Reserve collateral pub collateral: ReserveCollateral, /// Reserve configuration values pub config: ReserveConfig, @@ -46,6 +46,7 @@ impl Reserve { /// Initialize new reserve state pub fn new(params: NewReserveParams) -> Self { let NewReserveParams { + current_slot, lending_market, collateral, liquidity, @@ -55,7 +56,7 @@ impl Reserve { Self { version: PROGRAM_VERSION, - last_update: LastUpdate::new(), + last_update: LastUpdate::new(current_slot), cumulative_borrow_rate_wads: Decimal::one(), lending_market, collateral, @@ -65,6 +66,49 @@ impl Reserve { } } + /// 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 + .available_amount + .checked_add(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + self.collateral + .mint_total_supply + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + + 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)?; + // @FIXME: collateral represents a share of available liquidity, + // it should never be more than what's available + if liquidity_amount > self.liquidity.available_amount { + return Err(LendingError::InsufficientLiquidity.into()); + } + + self.liquidity.available_amount = self + .liquidity + .available_amount + .checked_sub(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + self.collateral.mint_total_supply = self + .collateral + .mint_total_supply + .checked_sub(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + + Ok(liquidity_amount) + } + /// Calculate the current borrow rate pub fn current_borrow_rate(&self) -> Result { let utilization_rate = self.liquidity.utilization_rate()?; @@ -101,6 +145,44 @@ impl Reserve { } } + /// 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 compounded_interest_rate = + self.compound_interest(current_borrow_rate, slots_elapsed)?; + self.liquidity.borrowed_amount_wads = self + .liquidity + .borrowed_amount_wads + .try_mul(compounded_interest_rate)?; + self.last_update.update_slot(current_slot); + } + Ok(()) + } + + /// Compound current borrow rate over elapsed slots + fn compound_interest( + &mut self, + current_borrow_rate: Rate, + slots_elapsed: u64, + ) -> Result { + let slot_interest_rate: 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)?; + Ok(compounded_interest_rate) + } + /// Borrow liquidity up to a maximum market value pub fn borrow_liquidity( &self, @@ -110,7 +192,7 @@ impl Reserve { loan_to_value_ratio: Rate, token_converter: impl TokenConverter, quote_token_mint: &Pubkey, - ) -> Result { + ) -> Result { let max_borrow_value = obligation .collateral_value .try_mul(loan_to_value_ratio)? @@ -132,7 +214,7 @@ impl Reserve { return Err(LendingError::BorrowTooLarge.into()); } - Ok(BorrowResult { + Ok(BorrowLiquidityResult { total_amount, borrow_amount, origination_fee, @@ -154,7 +236,7 @@ impl Reserve { .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; - Ok(BorrowResult { + Ok(BorrowLiquidityResult { total_amount, borrow_amount, origination_fee, @@ -169,7 +251,7 @@ impl Reserve { liquidity_amount: u64, liquidity_amount_type: AmountType, obligation_liquidity: &ObligationLiquidity, - ) -> Result { + ) -> Result { let settle_amount = match liquidity_amount_type { AmountType::ExactAmount => { Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) @@ -185,7 +267,7 @@ impl Reserve { settle_amount.try_floor_u64()? }; - Ok(RepayResult { + Ok(RepayLiquidityResult { settle_amount, repay_amount, }) @@ -199,7 +281,7 @@ impl Reserve { obligation: &Obligation, obligation_liquidity: &ObligationLiquidity, obligation_collateral: &ObligationCollateral, - ) -> Result { + ) -> Result { let max_withdraw_amount = Decimal::from(obligation_collateral.deposited_tokens); let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; @@ -230,7 +312,7 @@ impl Reserve { collateral_amount.try_floor_u64()? }; - return Ok(LiquidateResult { + return Ok(LiquidateObligationResult { settle_amount, repay_amount, withdraw_amount, @@ -270,102 +352,23 @@ impl Reserve { collateral_amount.try_floor_u64()? }; - Ok(LiquidateResult { + Ok(LiquidateObligationResult { settle_amount, repay_amount, withdraw_amount, }) } - - /// 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 - .available_amount - .checked_add(liquidity_amount) - .ok_or(LendingError::MathOverflow)?; - self.collateral - .mint_total_supply - .checked_add(collateral_amount) - .ok_or(LendingError::MathOverflow)?; - - 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)?; - // @FIXME: collateral represents a share of available liquidity, - // it should never be more than what's available - if liquidity_amount > self.liquidity.available_amount { - return Err(LendingError::InsufficientLiquidity.into()); - } - - self.liquidity.available_amount = self - .liquidity - .available_amount - .checked_sub(liquidity_amount) - .ok_or(LendingError::MathOverflow)?; - self.collateral.mint_total_supply = self - .collateral - .mint_total_supply - .checked_sub(collateral_amount) - .ok_or(LendingError::MathOverflow)?; - - Ok(liquidity_amount) - } - - /// 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 compounded_interest_rate = - self.compound_interest(current_borrow_rate, slots_elapsed)?; - self.liquidity.borrowed_amount_wads = self - .liquidity - .borrowed_amount_wads - .try_mul(compounded_interest_rate)?; - self.last_update.update_slot(current_slot); - } - Ok(()) - } - - /// Collateral exchange rate - pub fn collateral_exchange_rate(&self) -> Result { - let total_liquidity = self.liquidity.total_supply()?; - self.collateral.exchange_rate(total_liquidity) - } - - /// Compound current borrow rate over elapsed slots - fn compound_interest( - &mut self, - current_borrow_rate: Rate, - slots_elapsed: u64, - ) -> Result { - let slot_interest_rate: 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)?; - Ok(compounded_interest_rate) - } } /// Create new reserve pub struct NewReserveParams { + /// Current slot + pub current_slot: Slot, /// Lending market address pub lending_market: Pubkey, - /// Reserve collateral info + /// Reserve collateral pub collateral: ReserveCollateral, - /// Reserve liquidity info + /// Reserve liquidity pub liquidity: ReserveLiquidity, /// Optional dex market address pub dex_market: COption, @@ -373,9 +376,9 @@ pub struct NewReserveParams { pub config: ReserveConfig, } -/// Create borrow result +/// Borrow liquidity result #[derive(Debug)] -pub struct BorrowResult { +pub struct BorrowLiquidityResult { /// Total amount of borrow plus origination fee pub total_amount: u64, /// Borrow amount portion of total amount @@ -388,7 +391,7 @@ pub struct BorrowResult { /// Repay liquidity result #[derive(Debug)] -pub struct RepayResult { +pub struct RepayLiquidityResult { /// Amount of liquidity that is settled from the obligation. pub settle_amount: Decimal, /// Amount that will be repaid as u64 @@ -397,7 +400,7 @@ pub struct RepayResult { /// Liquidate obligation result #[derive(Debug)] -pub struct LiquidateResult { +pub struct LiquidateObligationResult { /// 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, @@ -426,7 +429,7 @@ pub struct ReserveLiquidity { } impl ReserveLiquidity { - /// New reserve liquidity info + /// New reserve liquidity pub fn new( mint_pubkey: Pubkey, mint_decimals: u8, @@ -654,15 +657,17 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 600; +const RESERVE_LEN: usize = 601; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 32 + 36 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 16 + 16 + 8 + 8 + 300 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, RESERVE_LEN]; + // @FIXME: reorder let ( version, last_update_slot, + last_update_stale, lending_market, liquidity_mint, liquidity_mint_decimals, @@ -684,10 +689,12 @@ impl Pack for Reserve { collateral_mint_supply, _padding, ) = mut_array_refs![ - output, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, + PUBKEY_LEN, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); + *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); pack_decimal( self.cumulative_borrow_rate_wads, cumulative_borrow_rate_wads, @@ -695,7 +702,7 @@ impl Pack for Reserve { lending_market.copy_from_slice(self.lending_market.as_ref()); pack_coption_key(&self.dex_market, dex_market); - // liquidity info + // liquidity liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); @@ -703,7 +710,7 @@ impl Pack for Reserve { *available_liquidity = self.liquidity.available_amount.to_le_bytes(); pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); - // collateral info + // collateral collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); @@ -725,6 +732,7 @@ impl Pack for Reserve { let ( version, last_update_slot, + last_update_stale, lending_market, liquidity_mint, liquidity_mint_decimals, @@ -746,12 +754,14 @@ impl Pack for Reserve { collateral_mint_supply, __padding, ) = array_refs![ - input, 1, 8, 32, 32, 1, 32, 32, 32, 32, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 32, 36, + 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; Ok(Self { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), + stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), lending_market: Pubkey::new_from_array(*lending_market), diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index c82eacbc350..95a2535152e 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -328,7 +328,9 @@ async fn test_withdraw_below_required() { .unwrap(), TransactionError::InstructionError( 3, - InstructionError::Custom(LendingError::ObligationLTVCannotGoAboveReserveLTV as u32) + InstructionError::Custom( + LendingError::ObligationLTVCannotGoAboveLendingMarketLTV as u32 + ) ) ); } From 3c1d27aefc9fafc8b8e64fbf9f6f52565a25d3db Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 18:14:56 -0500 Subject: [PATCH 085/191] interest is only accrued on refresh --- token-lending/program/src/processor.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 90373d2f2a4..e6560098f5c 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1550,9 +1550,6 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: is this necessary? - obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; - let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; if loan_to_value > loan_to_value_ratio { From 3da729e95948eca298dd9ba9edcdeafc469862f2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 19:11:14 -0500 Subject: [PATCH 086/191] fix borrow fees --- token-lending/program/src/processor.rs | 27 +++---- .../program/src/state/last_update.rs | 2 +- token-lending/program/src/state/obligation.rs | 6 +- .../src/state/obligation_collateral.rs | 26 +++---- .../program/src/state/obligation_liquidity.rs | 20 ++--- token-lending/program/src/state/reserve.rs | 77 ++++++++++--------- 6 files changed, 80 insertions(+), 78 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e6560098f5c..bdee90dc886 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1177,7 +1177,7 @@ fn process_withdraw_obligation_collateral( if obligation.last_update < obligation_collateral.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation_collateral.deposited_tokens == 0 { + if obligation_collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -1369,8 +1369,8 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidAccountInput.into()); } } - // @TODO: is this enough? borrow_reserve.cumulative_borrow_rate_wads is used below - // might need to restrict to current slot + // @FIXME: borrow_reserve.cumulative_borrow_rate_wads is used below + // need to refresh reserve or restrict to current slot if borrow_reserve.last_update.is_stale(clock.slot) { return Err(LendingError::ReserveStale.into()); } @@ -1570,9 +1570,9 @@ fn process_borrow_obligation_liquidity( )?; let BorrowLiquidityResult { - total_amount, borrow_amount, - origination_fee, + receive_amount, + borrow_fee, host_fee, } = borrow_reserve.borrow_liquidity( liquidity_amount, @@ -1583,13 +1583,8 @@ fn process_borrow_obligation_liquidity( &lending_market.quote_token_mint, )?; - // @FIXME: fees come out of liquidity pool, but they don't get paid by borrower - // @FIXME: don't we need to adjust total supply? - borrow_reserve - .liquidity - .borrow(total_amount, borrow_amount)?; - // @TODO: will this need further adjustment for fees? - obligation_liquidity.borrow(borrow_amount); + borrow_reserve.liquidity.borrow(borrow_amount)?; + obligation_liquidity.borrow(borrow_amount)?; obligation_liquidity.last_update.mark_stale(); obligation.last_update.mark_stale(); @@ -1600,7 +1595,7 @@ fn process_borrow_obligation_liquidity( Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - let mut owner_fee = origination_fee; + let mut owner_fee = borrow_fee; if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { if host_fee > 0 { owner_fee = owner_fee @@ -1631,7 +1626,7 @@ fn process_borrow_obligation_liquidity( spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: destination_liquidity_info.clone(), - amount: borrow_amount, + amount: receive_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), @@ -1926,7 +1921,7 @@ fn process_liquidate_obligation( if obligation.last_update < obligation_liquidity.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation_liquidity.borrowed_wads == Decimal::zero() { + if obligation_liquidity.borrowed_amount_wads == Decimal::zero() { return Err(LendingError::ObligationLiquidityEmpty.into()); } @@ -1963,7 +1958,7 @@ fn process_liquidate_obligation( if obligation.last_update < obligation_collateral.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation_collateral.deposited_tokens == 0 { + if obligation_collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index 49c51f4887e..4f1c80dff44 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -56,7 +56,7 @@ impl LastUpdate { impl PartialEq for LastUpdate { fn eq(&self, other: &Self) -> bool { - return &self.slot == &other.slot; + &self.slot == &other.slot } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 40ad9f80637..266d74eaa73 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -83,9 +83,9 @@ impl Obligation { let withdraw_amount = match collateral_amount_type { AmountType::ExactAmount => { - let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_tokens); + let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_amount); let withdraw_pct = Decimal::from(withdraw_amount) - .try_div(obligation_collateral.deposited_tokens)?; + .try_div(obligation_collateral.deposited_amount)?; let withdraw_value = self.collateral_value.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { return Err(LendingError::ObligationCollateralWithdrawTooLarge.into()); @@ -100,7 +100,7 @@ impl Obligation { .min(obligation_collateral.value); let withdraw_amount = withdraw_value .try_div(obligation_collateral.value)? - .try_mul(obligation_collateral.deposited_tokens)? + .try_mul(obligation_collateral.deposited_amount)? .try_floor_u64()?; withdraw_amount diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index 11e5f276ac4..db2aaff7e61 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -26,8 +26,8 @@ pub struct ObligationCollateral { pub deposit_reserve: Pubkey, /// Mint address of the tokens for this obligation collateral pub token_mint: Pubkey, - /// Amount of collateral tokens deposited for an obligation - pub deposited_tokens: u64, + /// Amount of collateral deposited + pub deposited_amount: u64, /// Market value of collateral in quote currency pub value: Decimal, } @@ -60,15 +60,15 @@ impl ObligationCollateral { obligation, deposit_reserve, token_mint, - deposited_tokens: 0, + deposited_amount: 0, value: Decimal::zero(), } } /// Increase deposited collateral pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult { - self.deposited_tokens = self - .deposited_tokens + self.deposited_amount = self + .deposited_amount .checked_add(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -76,8 +76,8 @@ impl ObligationCollateral { /// Decrease deposited collateral pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult { - self.deposited_tokens = self - .deposited_tokens + self.deposited_amount = self + .deposited_amount .checked_sub(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -91,7 +91,7 @@ impl ObligationCollateral { liquidity_token_mint: &Pubkey, ) -> ProgramResult { let liquidity_amount = collateral_exchange_rate - .decimal_collateral_to_liquidity(self.deposited_tokens.into())?; + .decimal_collateral_to_liquidity(self.deposited_amount.into())?; // @TODO: this may be slow/inaccurate for large amounts depending on dex market self.value = token_converter.convert(liquidity_amount, liquidity_token_mint)?; Ok(()) @@ -103,7 +103,7 @@ impl ObligationCollateral { collateral_amount: u64, obligation_token_supply: u64, ) -> Result { - let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_tokens)?; + let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_amount)?; withdraw_pct .try_mul(obligation_token_supply)? .try_floor_u64() @@ -130,7 +130,7 @@ impl Pack for ObligationCollateral { obligation, deposit_reserve, token_mint, - deposited_tokens, + deposited_amount, value, _padding, ) = mut_array_refs![output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; @@ -141,7 +141,7 @@ impl Pack for ObligationCollateral { obligation.copy_from_slice(self.obligation.as_ref()); deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); token_mint.copy_from_slice(self.token_mint.as_ref()); - *deposited_tokens = self.deposited_tokens.to_le_bytes(); + *deposited_amount = self.deposited_amount.to_le_bytes(); pack_decimal(self.value, value); } @@ -156,7 +156,7 @@ impl Pack for ObligationCollateral { obligation, deposit_reserve, token_mint, - deposited_tokens, + deposited_amount, value, _padding, ) = array_refs![input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; @@ -170,7 +170,7 @@ impl Pack for ObligationCollateral { obligation: Pubkey::new_from_array(*obligation), deposit_reserve: Pubkey::new_from_array(*deposit_reserve), token_mint: Pubkey::new_from_array(*token_mint), - deposited_tokens: u64::from_le_bytes(*deposited_tokens), + deposited_amount: u64::from_le_bytes(*deposited_amount), value: unpack_decimal(value), }) } diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index 5b271e247b4..a2f4036bb48 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -26,8 +26,8 @@ pub struct ObligationLiquidity { pub borrow_reserve: Pubkey, /// Borrow rate used for calculating interest pub cumulative_borrow_rate_wads: Decimal, - /// Amount of liquidity tokens borrowed for an obligation plus interest - pub borrowed_wads: Decimal, + /// Amount of liquidity borrowed plus interest + pub borrowed_amount_wads: Decimal, /// Market value of liquidity in quote currency pub value: Decimal, } @@ -57,20 +57,20 @@ impl ObligationLiquidity { obligation, borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), - borrowed_wads: Decimal::zero(), + borrowed_amount_wads: Decimal::zero(), value: Decimal::zero(), } } /// Decrease borrowed liquidity pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult { - self.borrowed_wads = self.borrowed_wads.try_sub(settle_amount)?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?; Ok(()) } /// Increase borrowed liquidity pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { - self.borrowed_wads = self.borrowed_wads.try_add(borrow_amount.into())?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; Ok(()) } @@ -84,7 +84,9 @@ impl ObligationLiquidity { .try_div(self.cumulative_borrow_rate_wads)? .try_into()?; - self.borrowed_wads = self.borrowed_wads.try_mul(compounded_interest_rate)?; + self.borrowed_amount_wads = self + .borrowed_amount_wads + .try_mul(compounded_interest_rate)?; self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; Ok(()) @@ -97,7 +99,7 @@ impl ObligationLiquidity { from_token_mint: &Pubkey, ) -> ProgramResult { // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.value = token_converter.convert(self.borrowed_wads, from_token_mint)?; + self.value = token_converter.convert(self.borrowed_amount_wads, from_token_mint)?; Ok(()) } } @@ -136,7 +138,7 @@ impl Pack for ObligationLiquidity { self.cumulative_borrow_rate_wads, cumulative_borrow_rate_wads, ); - pack_decimal(self.borrowed_wads, borrowed_wads); + pack_decimal(self.borrowed_amount_wads, borrowed_wads); pack_decimal(self.value, value); } @@ -165,7 +167,7 @@ impl Pack for ObligationLiquidity { obligation: Pubkey::new_from_array(*obligation), borrow_reserve: Pubkey::new_from_array(*borrow_reserve), cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), - borrowed_wads: unpack_decimal(borrowed_wads), + borrowed_amount_wads: unpack_decimal(borrowed_wads), value: unpack_decimal(value), }) } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 7df16fe3ae6..e5c6e7dff10 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -200,46 +200,46 @@ impl Reserve { match liquidity_amount_type { AmountType::ExactAmount => { - let borrow_amount = liquidity_amount; - let (origination_fee, host_fee) = self + let receive_amount = liquidity_amount; + let (borrow_fee, host_fee) = self .config .fees - .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; - let total_amount = borrow_amount - .checked_add(origination_fee) + .calculate_borrow_fees(receive_amount, FeeCalculation::Exclusive)?; + let borrow_amount = receive_amount + .checked_add(borrow_fee) .ok_or(LendingError::MathOverflow)?; - let total_value = - token_converter.convert(total_amount.into(), &self.liquidity.mint_pubkey)?; - if total_value > max_borrow_value { + let borrow_value = + token_converter.convert(borrow_amount.into(), &self.liquidity.mint_pubkey)?; + if borrow_value > max_borrow_value { return Err(LendingError::BorrowTooLarge.into()); } Ok(BorrowLiquidityResult { - total_amount, borrow_amount, - origination_fee, + receive_amount, + borrow_fee, host_fee, }) } AmountType::PercentAmount => { - let total_value = max_borrow_value - .try_mul(Decimal::from_percent(u8::try_from(liquidity_amount)?))?; - let total_amount = token_converter - .convert(total_value, "e_token_mint)? + let borrow_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); + let borrow_value = max_borrow_value.try_mul(borrow_pct)?; + let borrow_amount = token_converter + .convert(borrow_value, "e_token_mint)? .try_floor_u64()? .min(self.liquidity.available_amount); let (origination_fee, host_fee) = self .config .fees - .calculate_borrow_fees(total_amount, FeeCalculation::Inclusive)?; - let borrow_amount = total_amount + .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?; + let receive_amount = borrow_amount .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; Ok(BorrowLiquidityResult { - total_amount, borrow_amount, - origination_fee, + receive_amount, + borrow_fee: origination_fee, host_fee, }) } @@ -254,14 +254,16 @@ impl Reserve { ) -> Result { let settle_amount = match liquidity_amount_type { AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_amount_wads) } AmountType::PercentAmount => { let settle_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); - obligation_liquidity.borrowed_wads.try_mul(settle_pct)? + obligation_liquidity + .borrowed_amount_wads + .try_mul(settle_pct)? } }; - let repay_amount = if settle_amount == obligation_liquidity.borrowed_wads { + let repay_amount = if settle_amount == obligation_liquidity.borrowed_amount_wads { settle_amount.try_ceil_u64()? } else { settle_amount.try_floor_u64()? @@ -282,22 +284,24 @@ impl Reserve { obligation_liquidity: &ObligationLiquidity, obligation_collateral: &ObligationCollateral, ) -> Result { - let max_withdraw_amount = Decimal::from(obligation_collateral.deposited_tokens); + let max_withdraw_amount = Decimal::from(obligation_collateral.deposited_amount); let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; let liquidate_amount = match liquidity_amount_type { AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_wads) + Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_amount_wads) } AmountType::PercentAmount => { let liquidate_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); - obligation_liquidity.borrowed_wads.try_mul(liquidate_pct)? + obligation_liquidity + .borrowed_amount_wads + .try_mul(liquidate_pct)? } }; // Close out obligations that are too small to liquidate normally - if obligation_liquidity.borrowed_wads < LIQUIDATION_CLOSE_AMOUNT.into() { - let settle_amount = obligation_liquidity.borrowed_wads; + if obligation_liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() { + let settle_amount = obligation_liquidity.borrowed_amount_wads; let settle_value = obligation_liquidity .value .try_mul(bonus_rate)? @@ -325,11 +329,12 @@ impl Reserve { .min(obligation_liquidity.value); let max_liquidation_pct = max_liquidation_value.try_div(obligation_liquidity.value)?; let max_liquidation_amount = obligation_liquidity - .borrowed_wads + .borrowed_amount_wads .try_mul(max_liquidation_pct)?; let liquidation_amount = liquidate_amount.min(max_liquidation_amount); - let liquidation_pct = liquidation_amount.try_div(obligation_liquidity.borrowed_wads)?; + let liquidation_pct = + liquidation_amount.try_div(obligation_liquidity.borrowed_amount_wads)?; let settle_value = obligation_liquidity .value @@ -379,12 +384,12 @@ pub struct NewReserveParams { /// Borrow liquidity result #[derive(Debug)] pub struct BorrowLiquidityResult { - /// Total amount of borrow plus origination fee - pub total_amount: u64, - /// Borrow amount portion of total amount + /// Total amount of borrow including fees pub borrow_amount: u64, + /// Borrow amount portion of total amount + pub receive_amount: u64, /// Loan origination fee - pub origination_fee: u64, + pub borrow_fee: u64, /// Host fee portion of origination fee pub host_fee: u64, } @@ -451,15 +456,15 @@ impl ReserveLiquidity { Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads) } - /// Subtract total amount from available liquidity and add borrow amount to total borrows - pub fn borrow(&mut self, total_amount: u64, borrow_amount: u64) -> ProgramResult { - if total_amount > self.available_amount { + /// Subtract borrow amount from available liquidity and add to borrows + pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { + if borrow_amount > self.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } self.available_amount = self .available_amount - .checked_sub(total_amount) + .checked_sub(borrow_amount) .ok_or(LendingError::MathOverflow)?; self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; From 652cfcd0b138c63801d9ac0da912e92edd72515c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 19:19:50 -0500 Subject: [PATCH 087/191] 32 -> PUBKEY_LEN [skip ci] --- token-lending/program/src/state/lending_market.rs | 4 ++-- token-lending/program/src/state/mod.rs | 4 ++-- token-lending/program/src/state/reserve.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 8db04630753..c39d4f8e321 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -51,7 +51,7 @@ impl Pack for LendingMarket { loan_to_value_ratio, liquidation_threshold, _padding, - ) = array_refs![input, 1, 1, 32, 32, 32, 1, 1, 60]; + ) = array_refs![input, 1, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 1, 1, 60]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -80,7 +80,7 @@ impl Pack for LendingMarket { loan_to_value_ratio, liquidation_threshold, _padding, - ) = mut_array_refs![output, 1, 1, 32, 32, 32, 62]; + ) = mut_array_refs![output, 1, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 1, 1, 60]; *version = self.version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index da33c797ed8..6b5b6e95e1a 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -56,7 +56,7 @@ pub trait TokenConverter { // Helpers fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { - let (tag, body) = mut_array_refs![dst, 4, 32]; + let (tag, body) = mut_array_refs![dst, 4, PUBKEY_LEN]; match src { COption::Some(key) => { *tag = [1, 0, 0, 0]; @@ -69,7 +69,7 @@ fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { } fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, 32]; + let (tag, body) = array_refs![src, 4, PUBKEY_LEN]; match *tag { [0, 0, 0, 0] => Ok(COption::None), [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e5c6e7dff10..923e777ea08 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -759,8 +759,8 @@ impl Pack for Reserve { collateral_mint_supply, __padding, ) = array_refs![ - input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 32, 36, - 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, + PUBKEY_LEN, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; Ok(Self { version: u8::from_le_bytes(*version), From bbadd91942ff2063b194e3a8d6e336e9ea26e1a6 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 19:55:17 -0500 Subject: [PATCH 088/191] check rent exempt / uninitialized status first --- token-lending/program/src/processor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index bdee90dc886..5a4c0c4c8e4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -176,14 +176,14 @@ fn process_init_lending_market( let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, lending_market_info)?; + assert_uninitialized::(lending_market_info)?; + unpack_mint("e_token_mint_info.data.borrow())?; if quote_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } - assert_rent_exempt(rent, lending_market_info)?; - assert_uninitialized(lending_market_info)?; - let mut lending_market = LendingMarket { version: PROGRAM_VERSION, bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, @@ -278,14 +278,14 @@ fn process_init_reserve( let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, reserve_info)?; + assert_uninitialized::(reserve_info)?; + if reserve_liquidity_supply_info.key == source_liquidity_info.key { msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } - assert_rent_exempt(rent, reserve_info)?; - assert_uninitialized::(reserve_info)?; - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); From b3d58d87b8494c271df01f5c639647bf681c888a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 19:55:23 -0500 Subject: [PATCH 089/191] fixme comments --- token-lending/program/src/processor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 5a4c0c4c8e4..8a48be7303c 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -782,6 +782,7 @@ fn process_init_obligation_collateral( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + // @FIXME: unchecked math if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { return Err(LendingError::ObligationAccountLimit.into()); } @@ -1300,6 +1301,7 @@ fn process_init_obligation_liquidity( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + // @FIXME: unchecked math if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { return Err(LendingError::ObligationAccountLimit.into()); } From d9a3e707a030bdd7aea02020faf8bf1567d89e92 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 18 Mar 2021 20:02:49 -0500 Subject: [PATCH 090/191] PUBKEY_LEN, 32 -> PUBKEY_BYTES [skip ci] --- token-lending/program/src/dex_market.rs | 8 ++++-- token-lending/program/src/instruction.rs | 28 +++++++++---------- .../program/src/state/lending_market.rs | 6 ++-- token-lending/program/src/state/mod.rs | 13 ++++----- token-lending/program/src/state/obligation.rs | 24 ++++++++-------- .../src/state/obligation_collateral.rs | 6 ++-- .../program/src/state/obligation_liquidity.rs | 6 ++-- token-lending/program/src/state/reserve.rs | 10 +++---- 8 files changed, 51 insertions(+), 50 deletions(-) diff --git a/token-lending/program/src/dex_market.rs b/token-lending/program/src/dex_market.rs index b4095bfaa13..1ff9d40cc6b 100644 --- a/token-lending/program/src/dex_market.rs +++ b/token-lending/program/src/dex_market.rs @@ -7,7 +7,11 @@ use crate::{ }; use arrayref::{array_refs, mut_array_refs}; use serum_dex::critbit::{Slab, SlabView}; -use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::{Pubkey, PUBKEY_BYTES}, +}; use std::{cell::RefMut, convert::TryFrom}; /// Side of the dex market order book @@ -322,7 +326,7 @@ impl DexMarket { /// Get pubkey located at offset pub fn pubkey_at_offset(data: &[u8], offset: usize) -> Pubkey { let count_start = 5 + offset * 8; - let count_end = count_start + 32; + let count_end = count_start + PUBKEY_BYTES; Pubkey::new(&data[count_start..count_end]) } } diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index bbb91bce3f8..4a50b34f00c 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -9,7 +9,7 @@ use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, sysvar, }; use std::{convert::TryInto, mem::size_of}; @@ -502,8 +502,8 @@ impl LendingInstruction { } fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { - if input.len() >= 32 { - let (key, rest) = input.split_at(32); + if input.len() >= PUBKEY_BYTES { + let (key, rest) = input.split_at(PUBKEY_BYTES); let pk = Pubkey::new(key); Ok((pk, rest)) } else { @@ -688,7 +688,7 @@ pub fn init_reserve( dex_market_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); let mut accounts = vec![ AccountMeta::new(source_liquidity_pubkey, false), AccountMeta::new(destination_collateral_pubkey, false), @@ -734,7 +734,7 @@ pub fn deposit_reserve_liquidity( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -767,7 +767,7 @@ pub fn redeem_reserve_collateral( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -862,7 +862,7 @@ pub fn init_obligation_collateral( lending_market_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -894,7 +894,7 @@ pub fn refresh_obligation_collateral( memory_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -928,7 +928,7 @@ pub fn deposit_obligation_collateral( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -965,7 +965,7 @@ pub fn withdraw_obligation_collateral( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -1026,7 +1026,7 @@ pub fn refresh_obligation_liquidity( memory_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -1063,7 +1063,7 @@ pub fn borrow_obligation_liquidity( host_fee_receiver_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); let mut accounts = vec![ AccountMeta::new(source_liquidity_pubkey, false), AccountMeta::new(destination_liquidity_pubkey, false), @@ -1108,7 +1108,7 @@ pub fn repay_obligation_liquidity( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ @@ -1150,7 +1150,7 @@ pub fn liquidate_obligation( user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..32]], &program_id); + Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); Instruction { program_id, accounts: vec![ diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index c39d4f8e321..2f097ea74dc 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -3,7 +3,7 @@ use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ program_error::ProgramError, program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; /// Lending market state @@ -51,7 +51,7 @@ impl Pack for LendingMarket { loan_to_value_ratio, liquidation_threshold, _padding, - ) = array_refs![input, 1, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 1, 1, 60]; + ) = array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 1, 1, 60]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -80,7 +80,7 @@ impl Pack for LendingMarket { loan_to_value_ratio, liquidation_threshold, _padding, - ) = mut_array_refs![output, 1, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 1, 1, 60]; + ) = mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 1, 1, 60]; *version = self.version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 6b5b6e95e1a..d77c11f19dc 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -20,7 +20,7 @@ use solana_program::{ clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, program_error::ProgramError, program_option::COption, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) @@ -38,9 +38,6 @@ pub const UNINITIALIZED_VERSION: u8 = 0; pub const SLOTS_PER_YEAR: u64 = DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365; -/// Pubkey byte length -pub const PUBKEY_LEN: usize = 32; - /// Token converter pub trait TokenConverter { /// Return best price for specified token @@ -55,8 +52,8 @@ pub trait TokenConverter { } // Helpers -fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { - let (tag, body) = mut_array_refs![dst, 4, PUBKEY_LEN]; +fn pack_coption_key(src: &COption, dst: &mut [u8; 4 + PUBKEY_BYTES]) { + let (tag, body) = mut_array_refs![dst, 4, PUBKEY_BYTES]; match src { COption::Some(key) => { *tag = [1, 0, 0, 0]; @@ -68,8 +65,8 @@ fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { } } -fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, PUBKEY_LEN]; +fn unpack_coption_key(src: &[u8; 4 + PUBKEY_BYTES]) -> Result, ProgramError> { + let (tag, body) = array_refs![src, 4, PUBKEY_BYTES]; match *tag { [0, 0, 0, 0] => Ok(COption::None), [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 266d74eaa73..3bc29e8274a 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -10,7 +10,7 @@ use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::convert::{TryFrom, TryInto}; @@ -156,13 +156,13 @@ impl Pack for Obligation { 1, 8, 1, - PUBKEY_LEN, + PUBKEY_BYTES, 16, 16, 1, 1, - PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS, - PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS + PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS, + PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS ]; *version = self.version.to_le_bytes(); @@ -178,16 +178,16 @@ impl Pack for Obligation { let mut offset = 0; for pubkey in self.collateral.iter() { - let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; + let account = array_mut_ref![accounts_flat, offset, PUBKEY_BYTES]; account.copy_from_slice(pubkey.as_ref()); // @FIXME: unchecked math - offset += PUBKEY_LEN; + offset += PUBKEY_BYTES; } for pubkey in self.liquidity.iter() { - let account = array_mut_ref![accounts_flat, offset, PUBKEY_LEN]; + let account = array_mut_ref![accounts_flat, offset, PUBKEY_BYTES]; account.copy_from_slice(pubkey.as_ref()); // @FIXME: unchecked math - offset += PUBKEY_LEN; + offset += PUBKEY_BYTES; } } @@ -210,13 +210,13 @@ impl Pack for Obligation { 1, 8, 1, - PUBKEY_LEN, + PUBKEY_BYTES, 16, 16, 1, 1, - PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS, - PUBKEY_LEN * MAX_OBLIGATION_ACCOUNTS + PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS, + PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS ]; let collateral_len = u8::from_le_bytes(*num_collateral); @@ -230,7 +230,7 @@ impl Pack for Obligation { let mut offset = 0; // @TODO: is there a more idiomatic/performant way to iterate this? - for account in accounts_flat.chunks(PUBKEY_LEN) { + for account in accounts_flat.chunks(PUBKEY_BYTES) { if offset < collateral_len { collateral.push(Pubkey::new(account)); } else if offset < total_len { diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs index db2aaff7e61..7bbab6a5ab2 100644 --- a/token-lending/program/src/state/obligation_collateral.rs +++ b/token-lending/program/src/state/obligation_collateral.rs @@ -9,7 +9,7 @@ use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::convert::TryInto; @@ -133,7 +133,7 @@ impl Pack for ObligationCollateral { deposited_amount, value, _padding, - ) = mut_array_refs![output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; + ) = mut_array_refs![output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16, 128]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); @@ -159,7 +159,7 @@ impl Pack for ObligationCollateral { deposited_amount, value, _padding, - ) = array_refs![input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, 8, 16, 128]; + ) = array_refs![input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs index a2f4036bb48..b0280346c10 100644 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ b/token-lending/program/src/state/obligation_liquidity.rs @@ -9,7 +9,7 @@ use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::convert::TryInto; @@ -127,7 +127,7 @@ impl Pack for ObligationLiquidity { borrowed_wads, value, _padding, - ) = mut_array_refs![output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; + ) = mut_array_refs![output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 16, 16, 16, 128]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); @@ -156,7 +156,7 @@ impl Pack for ObligationLiquidity { borrowed_wads, value, _padding, - ) = array_refs![input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 16, 16, 16, 128]; + ) = array_refs![input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 16, 16, 16, 128]; Ok(Self { version: u8::from_le_bytes(*version), diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 923e777ea08..3fbddd2dd35 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -11,7 +11,7 @@ use solana_program::{ program_error::ProgramError, program_option::COption, program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::convert::{TryFrom, TryInto}; @@ -694,8 +694,8 @@ impl Pack for Reserve { collateral_mint_supply, _padding, ) = mut_array_refs![ - output, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, - PUBKEY_LEN, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, + PUBKEY_BYTES, 4 + PUBKEY_BYTES, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); @@ -759,8 +759,8 @@ impl Pack for Reserve { collateral_mint_supply, __padding, ) = array_refs![ - input, 1, 8, 1, PUBKEY_LEN, PUBKEY_LEN, 1, PUBKEY_LEN, PUBKEY_LEN, PUBKEY_LEN, - PUBKEY_LEN, 36, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, + PUBKEY_BYTES, 4 + PUBKEY_BYTES, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 ]; Ok(Self { version: u8::from_le_bytes(*version), From b30430b1f8efb7a9dd7b05644c9fe92d4c1b2309 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 19 Mar 2021 15:24:32 -0500 Subject: [PATCH 091/191] add solana-flux-aggregator --- Cargo.lock | 17 +++++++++++++++++ token-lending/program/Cargo.toml | 1 + 2 files changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 18d8e2ccd52..1d5daae5ef8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,6 +1043,22 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flux-aggregator" +version = "0.1.0" +source = "git+https://github.com/octopus-network/solana-flux-aggregator?rev=9cfaec5#9cfaec5a2977751c6cb8ceac8ee6cffe3a1b64c0" +dependencies = [ + "borsh 0.7.2", + "borsh-derive 0.7.2", + "byteorder", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "spl-token 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3915,6 +3931,7 @@ dependencies = [ "arrayref", "assert_matches", "base64 0.13.0", + "flux-aggregator", "log", "num-derive", "num-traits", diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index e87bf5e0e1e..6ccc2a30499 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -14,6 +14,7 @@ test-dump-genesis-accounts = [] [dependencies] arrayref = "0.3.6" +flux-aggregator = { git = "https://github.com/octopus-network/solana-flux-aggregator", rev = "9cfaec5", features = ["no-entrypoint"] } num-derive = "0.3" num-traits = "0.2" serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "991a86e", features = ["no-entrypoint"] } From 94d100443f9ea8fca18ff5a1b3cba86d3d53475f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 19 Mar 2021 15:35:21 -0500 Subject: [PATCH 092/191] first pass --- token-lending/program/src/dex_market.rs | 349 ------------------ token-lending/program/src/error.rs | 11 - token-lending/program/src/instruction.rs | 8 +- token-lending/program/src/lib.rs | 1 - token-lending/program/src/processor.rs | 99 +++-- .../program/src/state/lending_market.rs | 1 + token-lending/program/src/state/mod.rs | 13 - token-lending/program/src/state/reserve.rs | 216 +++++++---- 8 files changed, 194 insertions(+), 504 deletions(-) delete mode 100644 token-lending/program/src/dex_market.rs diff --git a/token-lending/program/src/dex_market.rs b/token-lending/program/src/dex_market.rs deleted file mode 100644 index 1ff9d40cc6b..00000000000 --- a/token-lending/program/src/dex_market.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! Dex market used for simulating trades - -use crate::{ - error::LendingError, - math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, - state::TokenConverter, -}; -use arrayref::{array_refs, mut_array_refs}; -use serum_dex::critbit::{Slab, SlabView}; -use solana_program::{ - account_info::AccountInfo, - program_error::ProgramError, - pubkey::{Pubkey, PUBKEY_BYTES}, -}; -use std::{cell::RefMut, convert::TryFrom}; - -/// Side of the dex market order book -#[derive(Clone, Copy, PartialEq)] -enum Side { - Bid, - Ask, -} - -/// Market currency -#[derive(Clone, Copy, PartialEq)] -enum Currency { - Base, - Quote, -} - -impl Currency { - fn opposite(&self) -> Self { - match self { - Currency::Base => Currency::Quote, - Currency::Quote => Currency::Base, - } - } -} - -/// Trade action for trade simulator -#[derive(PartialEq)] -pub enum TradeAction { - /// Sell tokens - Sell, - /// Buy tokens - Buy, -} - -/// Dex market order -struct Order { - price: u64, - quantity: u64, -} - -/// Trade simulator -pub struct TradeSimulator<'a> { - dex_market: DexMarket, - orders: DexMarketOrders<'a>, - orders_side: Side, - quote_token_mint: &'a Pubkey, - buy_token_mint: &'a Pubkey, - sell_token_mint: &'a Pubkey, -} - -impl<'a> TokenConverter for TradeSimulator<'a> { - fn best_price(&mut self, token_mint: &Pubkey) -> Result { - let action = if token_mint == self.buy_token_mint { - TradeAction::Buy - } else { - TradeAction::Sell - }; - - let currency = if token_mint == self.quote_token_mint { - Currency::Quote - } else { - Currency::Base - }; - - let order_book_side = match (action, currency) { - (TradeAction::Buy, Currency::Base) => Side::Ask, - (TradeAction::Sell, Currency::Quote) => Side::Ask, - (TradeAction::Buy, Currency::Quote) => Side::Bid, - (TradeAction::Sell, Currency::Base) => Side::Bid, - }; - if order_book_side != self.orders_side { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - - let best_order_price = self - .orders - .best_order_price() - .ok_or(LendingError::TradeSimulationError)?; - - let input_token = Decimal::one().try_div(self.dex_market.get_lots(currency))?; - let output_token_price = if currency == Currency::Base { - input_token.try_mul(best_order_price) - } else { - input_token.try_div(best_order_price) - }?; - Ok(output_token_price.try_mul(self.dex_market.get_lots(currency.opposite()))?) - } - - fn convert( - self, - from_amount: Decimal, - from_token_mint: &Pubkey, - ) -> Result { - let action = if from_token_mint == self.buy_token_mint { - TradeAction::Buy - } else { - TradeAction::Sell - }; - - self.simulate_trade(action, from_amount) - } -} - -impl<'a> TradeSimulator<'a> { - /// Create a new TradeSimulator - pub fn new( - dex_market_info: &AccountInfo, - dex_market_orders: &AccountInfo, - memory: &'a AccountInfo, - quote_token_mint: &'a Pubkey, - buy_token_mint: &'a Pubkey, - sell_token_mint: &'a Pubkey, - ) -> Result { - let dex_market = DexMarket::new(dex_market_info); - let orders = DexMarketOrders::new(&dex_market, dex_market_orders, memory)?; - let orders_side = orders.side; - - Ok(Self { - dex_market, - orders, - orders_side, - quote_token_mint, - buy_token_mint, - sell_token_mint, - }) - } - - /// Simulate a trade - pub fn simulate_trade( - mut self, - action: TradeAction, - quantity: Decimal, - ) -> Result { - let token_mint = match action { - TradeAction::Buy => self.buy_token_mint, - TradeAction::Sell => self.sell_token_mint, - }; - - let currency = if token_mint == self.quote_token_mint { - Currency::Quote - } else { - Currency::Base - }; - - let order_book_side = match (action, currency) { - (TradeAction::Buy, Currency::Base) => Side::Ask, - (TradeAction::Sell, Currency::Quote) => Side::Ask, - (TradeAction::Buy, Currency::Quote) => Side::Bid, - (TradeAction::Sell, Currency::Base) => Side::Bid, - }; - - if order_book_side != self.orders_side { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - - let input_quantity: Decimal = quantity.try_div(self.dex_market.get_lots(currency))?; - let output_quantity = self.exchange_with_order_book(input_quantity, currency)?; - Ok(output_quantity.try_mul(self.dex_market.get_lots(currency.opposite()))?) - } - - /// Exchange tokens by filling orders - fn exchange_with_order_book( - &mut self, - mut input_quantity: Decimal, - currency: Currency, - ) -> Result { - let mut output_quantity = Decimal::zero(); - - let zero = Decimal::zero(); - while input_quantity > zero { - let next_order = self - .orders - .next() - .ok_or_else(|| ProgramError::from(LendingError::TradeSimulationError))?; - - let next_order_price = next_order.price; - let base_quantity = next_order.quantity; - - let (filled, output) = if currency == Currency::Base { - let filled = input_quantity.min(Decimal::from(base_quantity)); - (filled, filled.try_mul(next_order_price)?) - } else { - let quote_quantity = Decimal::from(base_quantity).try_mul(next_order_price)?; - let filled = input_quantity.min(quote_quantity); - (filled, filled.try_div(next_order_price)?) - }; - - input_quantity = input_quantity.try_sub(filled)?; - output_quantity = output_quantity.try_add(output)?; - } - - Ok(output_quantity) - } -} - -/// Dex market order account info -struct DexMarketOrders<'a> { - heap: Option>, - side: Side, -} - -impl<'a> DexMarketOrders<'a> { - /// Create a new DexMarketOrders - fn new( - dex_market: &DexMarket, - orders: &AccountInfo, - memory: &'a AccountInfo, - ) -> Result { - let side = match orders.key { - key if key == &dex_market.bids => Side::Bid, - key if key == &dex_market.asks => Side::Ask, - _ => return Err(LendingError::DexInvalidOrderBookSide.into()), - }; - - if memory.data_len() < orders.data_len() { - return Err(LendingError::MemoryTooSmall.into()); - } - - let mut memory_data = memory.data.borrow_mut(); - fast_copy(&orders.data.borrow(), &mut memory_data); - let heap = Some(RefMut::map(memory_data, |bytes| { - // strip padding and header - let start = 5 + 8; - let end = bytes.len() - 7; - Slab::new(&mut bytes[start..end]) - })); - - Ok(Self { heap, side }) - } - - fn best_order_price(&mut self) -> Option { - let side = self.side; - self.heap.as_mut().and_then(|heap| { - let handle = match side { - Side::Bid => heap.find_max(), - Side::Ask => heap.find_min(), - }?; - - Some(heap.get_mut(handle)?.as_leaf_mut()?.price().get()) - }) - } -} - -impl Iterator for DexMarketOrders<'_> { - type Item = Order; - - fn next(&mut self) -> Option { - let leaf_node = match self.side { - Side::Bid => self.heap.as_mut().and_then(|heap| heap.remove_max()), - Side::Ask => self.heap.as_mut().and_then(|heap| heap.remove_min()), - }?; - - Some(Order { - price: leaf_node.price().get(), - quantity: leaf_node.quantity(), - }) - } -} - -/// Offset for dex market base mint -pub const BASE_MINT_OFFSET: usize = 6; -/// Offset for dex market quote mint -pub const QUOTE_MINT_OFFSET: usize = 10; - -const BIDS_OFFSET: usize = 35; -const ASKS_OFFSET: usize = 39; - -/// Dex market info -pub struct DexMarket { - bids: Pubkey, - asks: Pubkey, - base_lots: u64, - quote_lots: u64, -} - -impl DexMarket { - /// Create a new DexMarket - fn new(dex_market_info: &AccountInfo) -> Self { - let dex_market_data = dex_market_info.data.borrow(); - let bids = Self::pubkey_at_offset(&dex_market_data, BIDS_OFFSET); - let asks = Self::pubkey_at_offset(&dex_market_data, ASKS_OFFSET); - let base_lots = Self::base_lots(&dex_market_data); - let quote_lots = Self::quote_lots(&dex_market_data); - - Self { - bids, - asks, - base_lots, - quote_lots, - } - } - - fn get_lots(&self, currency: Currency) -> u64 { - match currency { - Currency::Base => self.base_lots, - Currency::Quote => self.quote_lots, - } - } - - fn base_lots(data: &[u8]) -> u64 { - let count_start = 5 + 43 * 8; - let count_end = count_start + 8; - u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) - } - - fn quote_lots(data: &[u8]) -> u64 { - let count_start = 5 + 44 * 8; - let count_end = count_start + 8; - u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) - } - - /// Get pubkey located at offset - pub fn pubkey_at_offset(data: &[u8], offset: usize) -> Pubkey { - let count_start = 5 + offset * 8; - let count_end = count_start + PUBKEY_BYTES; - Pubkey::new(&data[count_start..count_end]) - } -} - -/// A more efficient `copy_from_slice` implementation. -fn fast_copy(mut src: &[u8], mut dst: &mut [u8]) { - const COPY_SIZE: usize = 512; - while src.len() >= COPY_SIZE { - #[allow(clippy::ptr_offset_with_cast)] - let (src_word, src_rem) = array_refs![src, COPY_SIZE; ..;]; - #[allow(clippy::ptr_offset_with_cast)] - let (dst_word, dst_rem) = mut_array_refs![dst, COPY_SIZE; ..;]; - *dst_word = *src_word; - src = src_rem; - dst = dst_rem; - } - unsafe { - std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len()); - } -} diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index bc7e3481395..d2fdf7f5914 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -76,17 +76,6 @@ pub enum LendingError { #[error("Token burn failed")] TokenBurnFailed, - // 15 - /// The reserve lending market must be the same - #[error("Reserve mints do not match dex market mints")] - DexMarketMintMismatch, - /// Trade simulation error - #[error("Trade simulation error")] - TradeSimulationError, - /// Invalid dex order book side - #[error("Invalid dex order book side")] - DexInvalidOrderBookSide, - // @TODO: these are only used in one place that might be removed /// Input reserves cannot be the same #[error("Input reserves cannot be the same")] diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 4a50b34f00c..73830c9ef3b 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -77,7 +77,7 @@ pub enum LendingInstruction { /// 12 `[]` Clock sysvar /// 13 `[]` Rent sysvar /// 14 `[]` Token program id - /// 15 `[optional]` Serum DEX market account + /// 15 `[optional]` Flux Aggregator oracle account /// Not required for quote currency reserves. /// Must match quote and base currency. InitReserve { @@ -685,7 +685,7 @@ pub fn init_reserve( lending_market_pubkey: Pubkey, lending_market_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, - dex_market_pubkey: Option, + reserve_liquidity_aggregator_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); @@ -706,8 +706,8 @@ pub fn init_reserve( AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; - if let Some(dex_market_pubkey) = dex_market_pubkey { - accounts.push(AccountMeta::new_readonly(dex_market_pubkey, false)); + if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + accounts.push(AccountMeta::new_readonly(reserve_liquidity_aggregator_pubkey, false)); } Instruction { program_id, diff --git a/token-lending/program/src/lib.rs b/token-lending/program/src/lib.rs index 48b925d6961..196da20bc05 100644 --- a/token-lending/program/src/lib.rs +++ b/token-lending/program/src/lib.rs @@ -2,7 +2,6 @@ //! A lending program for the Solana blockchain. -pub mod dex_market; pub mod entrypoint; pub mod error; pub mod instruction; diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 8a48be7303c..7005639b79f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,7 +1,6 @@ //! Program state processor use crate::{ - dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, instruction::{init_lending_market, AmountType, LendingInstruction}, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, @@ -13,6 +12,7 @@ use crate::{ WithdrawCollateralResult, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, }, }; +use flux_aggregator; use num_traits::FromPrimitive; use solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -29,6 +29,7 @@ use solana_program::{ }; use spl_token::state::Account; use std::convert::TryFrom; +use crate::state::NewReserveLiquidityParams; /// Processes an instruction pub fn process_instruction( @@ -300,24 +301,17 @@ fn process_init_reserve( return Err(LendingError::InvalidSigner.into()); } - let dex_market = if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { - let dex_market_info = next_account_info(account_info_iter)?; - // TODO: check that market state is owned by real serum dex program - https://git.io/JmwJ1 - assert_rent_exempt(rent, dex_market_info)?; + let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { + let aggregator_info = next_account_info(account_info_iter)?; - let dex_market_data = &dex_market_info.data.borrow(); - let market_quote_mint = DexMarket::pubkey_at_offset(&dex_market_data, QUOTE_MINT_OFFSET); - if lending_market.quote_token_mint != market_quote_mint { - return Err(LendingError::DexMarketMintMismatch.into()); - } - let market_base_mint = DexMarket::pubkey_at_offset(&dex_market_data, BASE_MINT_OFFSET); - if reserve_liquidity_mint_info.key != &market_base_mint { - return Err(LendingError::DexMarketMintMismatch.into()); - } + assert_rent_exempt(rent, aggregator_info)?; + + // @TODO: is there a way to verify that aggregator_info uses the base and quote currency? + let answer = flux_aggregator::read_median(aggregator_info)?; - COption::Some(*dex_market_info.key) + (COption::Some(*aggregator_info.key), answer.median) } else { - COption::None + (COption::None, 1) }; let authority_signer_seeds = &[ @@ -335,12 +329,14 @@ fn process_init_reserve( return Err(LendingError::InvalidTokenOwner.into()); } - let reserve_liquidity = ReserveLiquidity::new( - *reserve_liquidity_mint_info.key, - reserve_liquidity_mint.decimals, - *reserve_liquidity_supply_info.key, - *reserve_liquidity_fee_receiver_info.key, - ); + let reserve_liquidity = ReserveLiquidity::new(NewReserveLiquidityParams { + mint_pubkey: *reserve_liquidity_mint_info.key, + mint_decimals: reserve_liquidity_mint.decimals, + supply_pubkey: *reserve_liquidity_supply_info.key, + fee_receiver: *reserve_liquidity_fee_receiver_info.key, + aggregator: reserve_liquidity_aggregator, + median_price: reserve_liquidity_median_price + }); let reserve_collateral = ReserveCollateral::new( *reserve_collateral_mint_info.key, *reserve_collateral_supply_info.key, @@ -350,7 +346,6 @@ fn process_init_reserve( lending_market: *lending_market_info.key, liquidity: reserve_liquidity, collateral: reserve_collateral, - dex_market, config, }); let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; @@ -417,6 +412,31 @@ fn process_init_reserve( Ok(()) } +// @TODO: support multiple reserves +fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let reserve_info = next_account_info(accounts_iter)?; + let reserve_liquidity_aggregator_info = next_account_info(accounts_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &reserve.liquidity.aggregator != reserve_liquidity_aggregator_info.key { + msg!("Invalid reserve liquidity aggregator account"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let answer = flux_aggregator::read_median(reserve_liquidity_aggregator_info)?; + + reserve.liquidity.median_price = answer.median; + reserve.accrue_interest(clock.slot)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + + Ok(()) +} + fn process_deposit_reserve_liquidity( program_id: &Pubkey, liquidity_amount: u64, @@ -599,6 +619,7 @@ fn process_redeem_reserve_collateral( Ok(()) } +// @FIMXE: remove, duplicated by refresh_reserve #[inline(never)] // avoid stack frame limit fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -906,16 +927,6 @@ fn process_refresh_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let token_converter = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - // @TODO: check these - &lending_market.quote_token_mint, - &deposit_reserve.liquidity.mint_pubkey, - )?; - obligation_collateral.update_value( deposit_reserve.collateral_exchange_rate()?, token_converter, @@ -1397,17 +1408,7 @@ fn process_refresh_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let token_converter = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - // @TODO: check these - &lending_market.quote_token_mint, - &borrow_reserve.liquidity.mint_pubkey, - )?; - - obligation_liquidity.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?; + obligation_liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; obligation_liquidity.update_value(token_converter, &borrow_reserve.liquidity.mint_pubkey)?; obligation_liquidity.last_update.update_slot(clock.slot); ObligationLiquidity::pack( @@ -1561,16 +1562,6 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::ObligationLTVCannotGoAboveLendingMarketLTV.into()); } - let token_converter = TradeSimulator::new( - dex_market_info, - dex_market_orders_info, - memory, - &lending_market.quote_token_mint, - // @TODO: check these - &borrow_reserve.liquidity.mint_pubkey, - &lending_market.quote_token_mint, - )?; - let BorrowLiquidityResult { borrow_amount, receive_amount, diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 2f097ea74dc..d12bd6a2b1c 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -6,6 +6,7 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; +// @FIXME: reorder /// Lending market state #[derive(Clone, Debug, Default, PartialEq)] pub struct LendingMarket { diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index d77c11f19dc..b3e45a0c405 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -38,19 +38,6 @@ pub const UNINITIALIZED_VERSION: u8 = 0; pub const SLOTS_PER_YEAR: u64 = DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365; -/// Token converter -pub trait TokenConverter { - /// Return best price for specified token - fn best_price(&mut self, token_mint: &Pubkey) -> Result; - - /// Convert between two different tokens - fn convert( - self, - from_amount: Decimal, - from_token_mint: &Pubkey, - ) -> Result; -} - // Helpers fn pack_coption_key(src: &COption, dst: &mut [u8; 4 + PUBKEY_BYTES]) { let (tag, body) = mut_array_refs![dst, 4, PUBKEY_BYTES]; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3fbddd2dd35..022f12dfc66 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -18,7 +18,7 @@ use std::convert::{TryFrom, TryInto}; /// Percentage of an obligation that can be repaid during each liquidation call pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50; -/// Obligation amount that is small enough to close out +/// Obligation borrow amount that is small enough to close out pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2; /// Lending market reserve state @@ -28,12 +28,8 @@ pub struct Reserve { pub version: u8, /// Last slot when supply and rates updated pub last_update: LastUpdate, - /// Cumulative borrow rate - pub cumulative_borrow_rate_wads: Decimal, /// Lending market address pub lending_market: Pubkey, - /// Dex market state account - pub dex_market: COption, /// Reserve liquidity pub liquidity: ReserveLiquidity, /// Reserve collateral @@ -48,20 +44,17 @@ impl Reserve { let NewReserveParams { current_slot, lending_market, - collateral, liquidity, - dex_market, + collateral, config, } = params; Self { version: PROGRAM_VERSION, last_update: LastUpdate::new(current_slot), - cumulative_borrow_rate_wads: Decimal::one(), lending_market, - collateral, liquidity, - dex_market, + collateral, config, } } @@ -156,33 +149,13 @@ impl Reserve { let slots_elapsed = self.last_update.slots_elapsed(current_slot)?; if slots_elapsed > 0 { let current_borrow_rate = self.current_borrow_rate()?; - let compounded_interest_rate = - self.compound_interest(current_borrow_rate, slots_elapsed)?; - self.liquidity.borrowed_amount_wads = self - .liquidity - .borrowed_amount_wads - .try_mul(compounded_interest_rate)?; + self.liquidity + .compound_interest(current_borrow_rate, slots_elapsed)?; self.last_update.update_slot(current_slot); } Ok(()) } - /// Compound current borrow rate over elapsed slots - fn compound_interest( - &mut self, - current_borrow_rate: Rate, - slots_elapsed: u64, - ) -> Result { - let slot_interest_rate: 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)?; - Ok(compounded_interest_rate) - } - /// Borrow liquidity up to a maximum market value pub fn borrow_liquidity( &self, @@ -375,8 +348,6 @@ pub struct NewReserveParams { pub collateral: ReserveCollateral, /// Reserve liquidity pub liquidity: ReserveLiquidity, - /// Optional dex market address - pub dex_market: COption, /// Reserve configuration values pub config: ReserveConfig, } @@ -416,7 +387,6 @@ pub struct LiquidateObligationResult { } /// Reserve liquidity -// @TODO: track market value in quote currency #[derive(Clone, Debug, Default, PartialEq)] pub struct ReserveLiquidity { /// Reserve liquidity mint address @@ -427,6 +397,12 @@ pub struct ReserveLiquidity { pub supply_pubkey: Pubkey, /// Reserve liquidity fee receiver address pub fee_receiver: Pubkey, + /// Optional reserve liquidity aggregator state account + pub aggregator: COption, + /// Reserve liquidity cumulative borrow rate + pub cumulative_borrow_rate_wads: Decimal, + /// Reserve liquidity median price + pub median_price: u64, /// Reserve liquidity available pub available_amount: u64, /// Reserve liquidity borrowed @@ -434,18 +410,24 @@ pub struct ReserveLiquidity { } impl ReserveLiquidity { - /// New reserve liquidity - pub fn new( - mint_pubkey: Pubkey, - mint_decimals: u8, - supply_pubkey: Pubkey, - fee_receiver: Pubkey, - ) -> Self { + /// Initialize new reserve state + pub fn new(params: NewReserveLiquidityParams) -> Self { + let NewReserveLiquidityParams { + mint_pubkey, + mint_decimals, + supply_pubkey, + fee_receiver, + aggregator, + median_price, + } = params; Self { mint_pubkey, mint_decimals, supply_pubkey, fee_receiver, + aggregator, + cumulative_borrow_rate_wads: Decimal::one(), + median_price, available_amount: 0, borrowed_amount_wads: Decimal::zero(), } @@ -490,6 +472,44 @@ impl ReserveLiquidity { } self.borrowed_amount_wads.try_div(total_supply)?.try_into() } + + /// Compound current borrow rate over elapsed slots + fn compound_interest( + &mut self, + current_borrow_rate: Rate, + slots_elapsed: u64, + ) -> ProgramResult { + let slot_interest_rate: 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)?; + self.borrowed_amount_wads = self + .borrowed_amount_wads + .try_mul(compounded_interest_rate)?; + Ok(()) + } +} + + + +/// Reserve liquidity +#[derive(Clone, Debug, Default, PartialEq)] +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 fee receiver address + pub fee_receiver: Pubkey, + /// Reserve liquidity aggregator state account + pub aggregator: COption, + /// Reserve liquidity median price + pub median_price: u64, } /// Reserve collateral @@ -662,13 +682,12 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 601; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 32 + 36 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 16 + 16 + 8 + 8 + 300 +const RESERVE_LEN: usize = 609; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 300 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, RESERVE_LEN]; - // @FIXME: reorder let ( version, last_update_slot, @@ -678,9 +697,14 @@ impl Pack for Reserve { liquidity_mint_decimals, liquidity_supply, liquidity_fee_receiver, - collateral_mint, + liquidity_aggregator, + liquidity_cumulative_borrow_rate_wads, + liquidity_median_price, + liquidity_available_amount, + liquidity_borrowed_amount_wads, collateral_supply, - dex_market, + collateral_mint, + collateral_mint_supply, optimal_utilization_rate, liquidation_bonus, min_borrow_rate, @@ -688,32 +712,55 @@ impl Pack for Reserve { max_borrow_rate, borrow_fee_wad, host_fee_percentage, - cumulative_borrow_rate_wads, - total_borrows, - available_liquidity, - collateral_mint_supply, _padding, ) = mut_array_refs![ - output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, - PUBKEY_BYTES, 4 + PUBKEY_BYTES, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 4 + PUBKEY_BYTES, + 16, + 8, + 8, + 16, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8, + 1, + 1, + 1, + 1, + 1, + 8, + 1, + 300 ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); - pack_decimal( - self.cumulative_borrow_rate_wads, - cumulative_borrow_rate_wads, - ); lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_coption_key(&self.dex_market, dex_market); // liquidity liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); - liquidity_fee_receiver.copy_from_slice(self.collateral.fee_receiver.as_ref()); - *available_liquidity = self.liquidity.available_amount.to_le_bytes(); - pack_decimal(self.liquidity.borrowed_amount_wads, total_borrows); + liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); + pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); + pack_decimal( + self.liquidity.cumulative_borrow_rate_wads, + liquidity_cumulative_borrow_rate_wads, + ); + *liquidity_median_price = self.liquidity.median_price.to_le_bytes(); + *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); + pack_decimal( + self.liquidity.borrowed_amount_wads, + liquidity_borrowed_amount_wads, + ); // collateral collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); @@ -743,9 +790,14 @@ impl Pack for Reserve { liquidity_mint_decimals, liquidity_supply, liquidity_fee_receiver, - collateral_mint, + liquidity_aggregator, + liquidity_cumulative_borrow_rate_wads, + liquidity_median_price, + liquidity_available_amount, + liquidity_borrowed_amount_wads, collateral_supply, - dex_market, + collateral_mint, + collateral_mint_supply, optimal_utilization_rate, liquidation_bonus, min_borrow_rate, @@ -753,14 +805,33 @@ impl Pack for Reserve { max_borrow_rate, borrow_fee_wad, host_fee_percentage, - cumulative_borrow_rate_wads, - total_borrows, - available_liquidity, - collateral_mint_supply, __padding, ) = array_refs![ - input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, - PUBKEY_BYTES, 4 + PUBKEY_BYTES, 1, 1, 1, 1, 1, 8, 1, 16, 16, 8, 8, 300 + input, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 4 + PUBKEY_BYTES, + 16, + 8, + 8, + 16, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8, + 1, + 1, + 1, + 1, + 1, + 8, + 1, + 300 ]; Ok(Self { version: u8::from_le_bytes(*version), @@ -768,16 +839,17 @@ impl Pack for Reserve { slot: u64::from_le_bytes(*last_update_slot), stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), lending_market: Pubkey::new_from_array(*lending_market), - dex_market: unpack_coption_key(dex_market)?, liquidity: ReserveLiquidity { mint_pubkey: Pubkey::new_from_array(*liquidity_mint), mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), supply_pubkey: Pubkey::new_from_array(*liquidity_supply), fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), - available_amount: u64::from_le_bytes(*available_liquidity), - borrowed_amount_wads: unpack_decimal(total_borrows), + aggregator: unpack_coption_key(liquidity_aggregator)?, + cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), + median_price: u64::from_le_bytes(*liquidity_median_price), + available_amount: u64::from_le_bytes(*liquidity_available_amount), + borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), }, collateral: ReserveCollateral { mint_pubkey: Pubkey::new_from_array(*collateral_mint), From a4a417bf8019d68d1ee83edea882a86553aa656d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 10:20:18 -0500 Subject: [PATCH 093/191] oracle wip --- token-lending/program/src/error.rs | 25 +- token-lending/program/src/instruction.rs | 442 +++------- token-lending/program/src/processor.rs | 826 +++--------------- .../program/src/state/last_update.rs | 2 +- token-lending/program/src/state/mod.rs | 4 - token-lending/program/src/state/obligation.rs | 408 ++++----- .../src/state/obligation_collateral.rs | 177 ---- .../program/src/state/obligation_liquidity.rs | 174 ---- token-lending/program/src/state/reserve.rs | 619 +------------ 9 files changed, 458 insertions(+), 2219 deletions(-) delete mode 100644 token-lending/program/src/state/obligation_collateral.rs delete mode 100644 token-lending/program/src/state/obligation_liquidity.rs diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index d2fdf7f5914..04206f9e19e 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -103,9 +103,12 @@ pub enum LendingError { /// Borrow amount too large #[error("Borrow amount too large for deposited collateral")] BorrowTooLarge, + + // @FIXME: name + message /// Liquidation amount too small #[error("Liquidation amount too small to receive collateral")] LiquidationTooSmall, + // @FIXME: name + message /// Cannot liquidate healthy obligations #[error("Cannot liquidate healthy obligations")] ObligationHealthy, @@ -117,10 +120,7 @@ pub enum LendingError { /// Obligation state stale #[error("Obligation state needs to be refreshed")] ObligationStale, - // @FIXME: not used anywhere, maybe should be? - /// The obligation token owner must be the same if reusing an obligation - #[error("Obligation token owner mismatch")] - ObligationTokenOwnerMismatch, + // @FIXME: change name + message /// Obligation LTV is above the lending market LTV #[error("Obligation LTV is above the lending market LTV")] @@ -130,30 +130,25 @@ pub enum LendingError { #[error("Obligation LTV cannot go above the lending market LTV")] ObligationLTVCannotGoAboveLendingMarketLTV, + /// Invalid obligation collateral + #[error("Invalid obligation collateral")] + InvalidObligationCollateral, /// Obligation collateral is empty #[error("Obligation collateral is empty")] ObligationCollateralEmpty, - /// Obligation collateral stale - #[error("Obligation collateral state needs to be refreshed")] - ObligationCollateralStale, /// Obligation collateral withdraw too large #[error("Obligation collateral withdraw too large for borrowed liquidity")] ObligationCollateralWithdrawTooLarge, - // @FIXME: change name + message - /// ObligationCollateralDuplicate - #[error("ObligationCollateralDuplicate")] - ObligationCollateralDuplicate, + /// Invalid obligation liquidity + #[error("Invalid obligation liquidity")] + InvalidObligationLiquidity, /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, /// Obligation liquidity stale #[error("Obligation liquidity state needs to be refreshed")] ObligationLiquidityStale, - // @FIXME: change name + message - /// ObligationLiquidityDuplicate - #[error("ObligationLiquidityDuplicate")] - ObligationLiquidityDuplicate, /// Negative interest rate #[error("Interest rate is negative")] diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 73830c9ef3b..225df70ff76 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -31,7 +31,7 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Lending market account + /// 0. `[writable]` Lending market account - uninitialized /// 1. `[]` Quote currency SPL Token mint /// 2. `[]` Rent sysvar /// 3. `[]` Token program id @@ -64,12 +64,12 @@ pub enum LendingInstruction { /// 0. `[writable]` Source liquidity token account /// $authority can transfer $liquidity_amount. /// 1. `[writable]` Destination collateral token account - uninitialized - /// 2. `[writable]` Reserve account + /// 2. `[writable]` Reserve account - uninitialized /// 3. `[]` Reserve liquidity SPL Token mint /// 4. `[writable]` Reserve liquidity supply SPL Token account - uninitialized - /// 7. `[writable]` Reserve liquidity fee receiver - uninitialized - /// 5. `[writable]` Reserve collateral SPL Token mint - uninitialized - /// 6. `[writable]` Reserve collateral token supply - uninitialized + /// 5. `[writable]` Reserve liquidity fee receiver - uninitialized + /// 6. `[writable]` Reserve collateral SPL Token mint - uninitialized + /// 7. `[writable]` Reserve collateral token supply - uninitialized /// 8. `[]` Lending market account /// 9. `[signer]` Lending market owner /// 10 `[]` Derived lending market authority @@ -77,7 +77,7 @@ pub enum LendingInstruction { /// 12 `[]` Clock sysvar /// 13 `[]` Rent sysvar /// 14 `[]` Token program id - /// 15 `[optional]` Flux Aggregator oracle account + /// 15 `[optional]` Reserve liquidity aggregator account /// Not required for quote currency reserves. /// Must match quote and base currency. InitReserve { @@ -88,8 +88,20 @@ pub enum LendingInstruction { }, // 3 - /// Deposit liquidity into a reserve in exchange for collateral representing ownership of the - /// reserve liquidity pool. + /// Accrue interest and update median quote token price on reserves. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` Clock sysvar + /// 1. `[writable]` Reserve account + /// 2. `[]` Reserve liquidity aggregator account + /// .. `[writable]` Additional reserve account + /// .. `[]` Additional reserve liquidity aggregator account + RefreshReserve, + + // 4 + /// Deposit liquidity into a reserve in exchange for collateral. Collateral represents a share + /// of the reserve liquidity pool. /// /// Accounts expected by this instruction: /// @@ -109,7 +121,7 @@ pub enum LendingInstruction { liquidity_amount: u64, }, - // 4 + // 5 /// Redeem collateral from a reserve in exchange for liquidity. /// /// Accounts expected by this instruction: @@ -129,16 +141,6 @@ pub enum LendingInstruction { collateral_amount: u64, }, - // 5 - /// Accrue interest on reserves - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` Clock sysvar - /// 1. `[writable]` Reserve account - /// .. `[writable]` Additional reserve accounts - AccrueReserveInterest, - // 6 /// Initializes a new lending market obligation. /// @@ -152,7 +154,7 @@ pub enum LendingInstruction { InitObligation, // 7 - /// Refresh an obligation's loan to value ratio. + /// Refresh an obligation's loan to value ratio. Requires refreshed reserves. /// /// Accounts expected by this instruction: /// @@ -160,48 +162,12 @@ pub enum LendingInstruction { /// 1. `[]` Lending market account /// 2. `[]` Clock sysvar /// 3. `[]` Token program id - /// 4..4+N `[]` Obligation collateral and liquidity accounts - /// Must be all initialized collateral accounts in exact order, followed by - /// all initialized liquidity accounts in exact order, with no additional - /// accounts following. + /// .. `[]` Collateral deposit reserve accounts - refreshed + /// .. `[]` Liquidity borrow reserve accounts - refreshed RefreshObligation, // 8 - /// Initializes a new obligation collateral. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account - /// 1. `[writable]` Obligation collateral account - uninitialized - /// 2. `[]` Deposit reserve account - /// 3. `[writable]` Obligation token mint - uninitialized - /// 4. `[writable]` Obligation token output account - /// 5. `[]` Obligation token owner - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[]` Clock sysvar - /// 9. `[]` Rent sysvar - /// 10 `[]` Token program id - InitObligationCollateral, - - // 9 - /// Refresh market value of an obligation's collateral. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation collateral account - /// 1. `[]` Deposit reserve account - /// 2. `[]` Lending market account - /// 3. `[]` Derived lending market authority - /// 4. `[]` Dex market - /// 5. `[]` Dex market order book side - /// 6. `[]` Temporary memory - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program id - RefreshObligationCollateral, - - // 10 - /// Deposit collateral to an obligation. + /// Deposit collateral to an obligation. Requires a refreshed reserve. /// /// Accounts expected by this instruction: /// @@ -209,21 +175,21 @@ pub enum LendingInstruction { /// Minted by deposit reserve collateral mint. /// $authority can transfer $collateral_amount. /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account - /// 2. `[]` Deposit reserve account + /// 2. `[]` Deposit reserve account - refreshed /// 3. `[writable]` Obligation account - /// 4. `[writable]` Obligation collateral account - /// 5. `[writable]` Obligation token mint - /// 6. `[writable]` Obligation token output account - /// 7. `[]` Lending market account - /// 8. `[]` Derived lending market authority - /// 9. `[signer]` User transfer authority ($authority) + /// 4. `[writable]` Obligation token mint + /// 5. `[writable]` Obligation token output account + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[signer]` User transfer authority ($authority) + /// 9. `[]` Clock sysvar /// 10 `[]` Token program id DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, }, - // 11 + // 9 /// Withdraw collateral from an obligation. The loan must remain healthy. Requires a /// recently refreshed obligation. /// @@ -235,14 +201,13 @@ pub enum LendingInstruction { /// $authority can transfer $collateral_amount. /// 2. `[]` Withdraw reserve account /// 3. `[writable]` Obligation account - /// 4. `[writable]` Obligation collateral account - /// 5. `[writable]` Obligation token mint - /// 6. `[writable]` Obligation token input account - /// 7. `[]` Lending market account - /// 8. `[]` Derived lending market authority - /// 9. `[signer]` User transfer authority ($authority) - /// 10 `[]` Clock sysvar - /// 11 `[]` Token program id + /// 4. `[writable]` Obligation token mint + /// 5. `[writable]` Obligation token input account + /// 6. `[]` Lending market account + /// 7. `[]` Derived lending market authority + /// 8. `[signer]` User transfer authority ($authority) + /// 9. `[]` Clock sysvar + /// 10 `[]` Token program id WithdrawObligationCollateral { /// Amount of collateral to withdraw - usage depends on `collateral_amount_type` collateral_amount: u64, @@ -250,37 +215,7 @@ pub enum LendingInstruction { collateral_amount_type: AmountType, }, - // 12 - /// Initializes a new obligation liquidity. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account - /// 1. `[writable]` Obligation liquidity account - uninitialized - /// 2. `[]` Borrow reserve account - /// 3. `[]` Lending market account - /// 4. `[]` Clock sysvar - /// 5. `[]` Rent sysvar - /// 6. `[]` Token program id - InitObligationLiquidity, - - // 13 - /// Refresh market value and accrued interest of an obligation's liquidity. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation liquidity account - /// 1. `[]` Borrow reserve account - /// 2. `[]` Lending market account - /// 3. `[]` Derived lending market authority - /// 4. `[]` Dex market - /// 5. `[]` Dex market order book side - /// 6. `[]` Temporary memory - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program id - RefreshObligationLiquidity, - - // 14 + // 10 /// Borrow liquidity from a reserve by depositing collateral tokens. The amount of liquidity is /// determined by market price. Requires a recently refreshed obligation. /// @@ -293,15 +228,11 @@ pub enum LendingInstruction { /// 3. `[writable]` Borrow reserve liquidity fee receiver account /// Must be the fee account specified at InitReserve. /// 4. `[writable]` Obligation account - /// 5. `[writable]` Obligation liquidity account - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[]` Dex market - /// 9. `[]` Dex market order book side - /// 10 `[]` Temporary memory - /// 11 `[]` Clock sysvar - /// 12 `[]` Token program id - /// 13 `[optional, writable]` Host fee receiver account + /// 5. `[]` Lending market account + /// 6. `[]` Derived lending market authority + /// 7. `[]` Clock sysvar + /// 8. `[]` Token program id + /// 9. `[optional, writable]` Host fee receiver account BorrowObligationLiquidity { /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -310,7 +241,7 @@ pub enum LendingInstruction { // TODO: slippage constraint - https://git.io/JmV67 }, - // 15 + // 11 /// Repay borrowed liquidity to a reserve. Requires a recently refreshed obligation. /// /// Accounts expected by this instruction: @@ -321,12 +252,11 @@ pub enum LendingInstruction { /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account /// 2. `[writable]` Repay reserve account /// 3. `[writable]` Obligation account - /// 4. `[writable]` Obligation liquidity account - /// 5. `[]` Lending market account - /// 6. `[]` Derived lending market authority - /// 7. `[signer]` User transfer authority ($authority) - /// 8. `[]` Clock sysvar - /// 9. `[]` Token program id + /// 4. `[]` Lending market account + /// 5. `[]` Derived lending market authority + /// 6. `[signer]` User transfer authority ($authority) + /// 7. `[]` Clock sysvar + /// 8. `[]` Token program id RepayObligationLiquidity { /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -334,7 +264,7 @@ pub enum LendingInstruction { liquidity_amount_type: AmountType, }, - // 16 + // 12 /// Repay borrowed liquidity to a reserve to receive collateral at a discount from an unhealthy /// obligation. Requires a recently refreshed obligation. /// @@ -350,13 +280,11 @@ pub enum LendingInstruction { /// 4. `[writable]` Withdraw reserve account /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account /// 6. `[writable]` Obligation account - /// 7. `[writable]` Obligation liquidity account - /// 8. `[writable]` Obligation collateral account - /// 9. `[]` Lending market account - /// 10 `[]` Derived lending market authority - /// 11 `[signer]` User transfer authority ($authority) - /// 12 `[]` Clock sysvar - /// 13 `[]` Token program id + /// 7. `[]` Lending market account + /// 8. `[]` Derived lending market authority + /// 9. `[signer]` User transfer authority ($authority) + /// 10 `[]` Clock sysvar + /// 11 `[]` Token program id LiquidateObligation { /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -410,24 +338,22 @@ impl LendingInstruction { }, } } - 3 => { + 3 => Self::RefreshReserve, + 4 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::DepositReserveLiquidity { liquidity_amount } } - 4 => { + 5 => { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; Self::RedeemReserveCollateral { collateral_amount } } - 5 => Self::AccrueReserveInterest, 6 => Self::InitObligation, 7 => Self::RefreshObligation, - 8 => Self::InitObligationCollateral, - 9 => Self::RefreshObligationCollateral, - 10 => { + 8 => { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; Self::DepositObligationCollateral { collateral_amount } } - 11 => { + 9 => { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; let (collateral_amount_type, _rest) = Self::unpack_u8(rest)?; let collateral_amount_type = AmountType::from_u8(collateral_amount_type) @@ -437,9 +363,7 @@ impl LendingInstruction { collateral_amount_type, } } - 12 => Self::InitObligationLiquidity, - 13 => Self::RefreshObligationLiquidity, - 14 => { + 10 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) @@ -449,7 +373,7 @@ impl LendingInstruction { liquidity_amount_type, } } - 15 => { + 11 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) @@ -459,7 +383,7 @@ impl LendingInstruction { liquidity_amount_type, } } - 16 => { + 12 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) @@ -555,16 +479,16 @@ impl LendingInstruction { buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } - Self::DepositReserveLiquidity { liquidity_amount } => { + Self::RefreshReserve => { buf.push(3); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::RedeemReserveCollateral { collateral_amount } => { + Self::DepositReserveLiquidity { liquidity_amount } => { buf.push(4); - buf.extend_from_slice(&collateral_amount.to_le_bytes()); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::AccrueReserveInterest => { + Self::RedeemReserveCollateral { collateral_amount } => { buf.push(5); + buf.extend_from_slice(&collateral_amount.to_le_bytes()); } Self::InitObligation => { buf.push(6); @@ -572,35 +496,23 @@ impl LendingInstruction { Self::RefreshObligation => { buf.push(7); } - Self::InitObligationCollateral => { - buf.push(8); - } - Self::RefreshObligationCollateral => { - buf.push(9); - } Self::DepositObligationCollateral { collateral_amount } => { - buf.push(10); + buf.push(8); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } Self::WithdrawObligationCollateral { collateral_amount, collateral_amount_type, } => { - buf.push(11); + buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); buf.extend_from_slice(&collateral_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::InitObligationLiquidity => { - buf.push(12); - } - Self::RefreshObligationLiquidity => { - buf.push(13); - } Self::BorrowObligationLiquidity { liquidity_amount, liquidity_amount_type, } => { - buf.push(14); + buf.push(10); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } @@ -608,7 +520,7 @@ impl LendingInstruction { liquidity_amount, liquidity_amount_type, } => { - buf.push(15); + buf.push(11); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } @@ -616,7 +528,7 @@ impl LendingInstruction { liquidity_amount, liquidity_amount_type, } => { - buf.push(16); + buf.push(12); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } @@ -687,8 +599,10 @@ pub fn init_reserve( user_transfer_authority_pubkey: Pubkey, reserve_liquidity_aggregator_pubkey: Option, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); let mut accounts = vec![ AccountMeta::new(source_liquidity_pubkey, false), AccountMeta::new(destination_collateral_pubkey, false), @@ -707,7 +621,10 @@ pub fn init_reserve( AccountMeta::new_readonly(spl_token::id(), false), ]; if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { - accounts.push(AccountMeta::new_readonly(reserve_liquidity_aggregator_pubkey, false)); + accounts.push(AccountMeta::new_readonly( + reserve_liquidity_aggregator_pubkey, + false, + )); } Instruction { program_id, @@ -733,8 +650,10 @@ pub fn deposit_reserve_liquidity( lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -766,8 +685,10 @@ pub fn redeem_reserve_collateral( lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -786,8 +707,9 @@ pub fn redeem_reserve_collateral( } } -/// Creates an `AccrueReserveInterest` instruction -pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { +// @FIXME: needs optional reserve liquidity aggregator pubkeys +/// Creates a `RefreshReserve` instruction +pub fn refresh_reserve(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { let mut accounts = Vec::with_capacity(1 + reserve_pubkeys.len()); accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)); accounts.extend( @@ -798,7 +720,7 @@ pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec) Instruction { program_id, accounts, - data: LendingInstruction::AccrueReserveInterest.pack(), + data: LendingInstruction::RefreshReserve.pack(), } } @@ -828,9 +750,9 @@ pub fn refresh_obligation( program_id: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, - obligation_collateral_liquidity_pubkeys: Vec, + reserve_pubkeys: Vec, ) -> Instruction { - let mut accounts = Vec::with_capacity(4 + obligation_collateral_liquidity_pubkeys.len()); + let mut accounts = Vec::with_capacity(4 + reserve_pubkeys.len()); accounts.extend(vec![ AccountMeta::new(obligation_pubkey, false) AccountMeta::new_readonly(lending_market_pubkey, false), @@ -838,7 +760,7 @@ pub fn refresh_obligation( AccountMeta::new_readonly(spl_token::id(), false), ]); accounts.extend( - obligation_collateral_liquidity_pubkeys + reserve_pubkeys .into_iter() .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), ); @@ -849,69 +771,6 @@ pub fn refresh_obligation( } } -/// Creates an 'InitObligationCollateral' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_obligation_collateral( - program_id: Pubkey, - obligation_pubkey: Pubkey, - obligation_collateral_pubkey: Pubkey, - deposit_reserve_pubkey: Pubkey, - obligation_token_mint_pubkey: Pubkey, - obligation_token_output_pubkey: Pubkey, - obligation_token_owner_pubkey: Pubkey, - lending_market_pubkey: Pubkey, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_collateral_pubkey, false), - AccountMeta::new_readonly(deposit_reserve_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), - AccountMeta::new_readonly(obligation_token_owner_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::InitObligationCollateral.pack(), - } -} - -/// Creates a 'RefreshObligationCollateral' instruction. -#[allow(clippy::too_many_arguments)] -pub fn refresh_obligation_collateral( - program_id: Pubkey, - obligation_collateral_pubkey: Pubkey, - deposit_reserve_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_collateral_pubkey, false), - AccountMeta::new_readonly(deposit_reserve_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::RefreshObligationCollateral.pack(), - } -} - /// Creates a 'DepositObligationCollateral' instruction. #[allow(clippy::too_many_arguments)] pub fn deposit_obligation_collateral( @@ -921,14 +780,15 @@ pub fn deposit_obligation_collateral( destination_collateral_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_collateral_pubkey: Pubkey, obligation_mint_pubkey: Pubkey, obligation_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -936,12 +796,12 @@ pub fn deposit_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new(obligation_mint_pubkey, false), AccountMeta::new(obligation_output_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), @@ -958,14 +818,15 @@ pub fn withdraw_obligation_collateral( destination_collateral_pubkey: Pubkey, withdraw_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_collateral_pubkey: Pubkey, obligation_mint_pubkey: Pubkey, obligation_input_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -973,7 +834,6 @@ pub fn withdraw_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(withdraw_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new(obligation_mint_pubkey, false), AccountMeta::new(obligation_input_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), @@ -990,60 +850,6 @@ pub fn withdraw_obligation_collateral( } } -/// Creates an 'InitObligationLiquidity' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_obligation_liquidity( - program_id: Pubkey, - obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - lending_market_pubkey: Pubkey, -) -> Instruction { - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), - AccountMeta::new_readonly(borrow_reserve_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::InitObligationLiquidity.pack(), - } -} - -/// Creates a 'RefreshObligationLiquidity' instruction. -#[allow(clippy::too_many_arguments)] -pub fn refresh_obligation_liquidity( - program_id: Pubkey, - obligation_liquidity_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(obligation_liquidity_pubkey, false), - AccountMeta::new_readonly(borrow_reserve_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::RefreshObligationLiquidity.pack(), - } -} - /// Creates a 'BorrowObligationLiquidity' instruction. #[allow(clippy::too_many_arguments)] pub fn borrow_obligation_liquidity( @@ -1055,27 +861,21 @@ pub fn borrow_obligation_liquidity( borrow_reserve_pubkey: Pubkey, borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, lending_market_pubkey: Pubkey, - dex_market_pubkey: Pubkey, - dex_market_order_book_side_pubkey: Pubkey, - memory_pubkey: Pubkey, host_fee_receiver_pubkey: Option, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); let mut accounts = vec![ AccountMeta::new(source_liquidity_pubkey, false), AccountMeta::new(destination_liquidity_pubkey, false), AccountMeta::new(borrow_reserve_pubkey, false), AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(dex_market_pubkey, false), - AccountMeta::new_readonly(dex_market_order_book_side_pubkey, false), - AccountMeta::new_readonly(memory_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; @@ -1103,12 +903,13 @@ pub fn repay_obligation_liquidity( destination_liquidity_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -1116,7 +917,6 @@ pub fn repay_obligation_liquidity( AccountMeta::new(destination_liquidity_pubkey, false), AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), @@ -1144,13 +944,13 @@ pub fn liquidate_obligation( withdraw_reserve_pubkey: Pubkey, withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_liquidity_pubkey: Pubkey, - obligation_collateral_pubkey: Pubkey, lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id); + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); Instruction { program_id, accounts: vec![ @@ -1161,8 +961,6 @@ pub fn liquidate_obligation( AccountMeta::new(withdraw_reserve_pubkey, false), AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_liquidity_pubkey, false), - AccountMeta::new(obligation_collateral_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7005639b79f..b05be5370d6 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,15 +1,15 @@ //! Program state processor +use crate::state::NewReserveLiquidityParams; use crate::{ error::LendingError, instruction::{init_lending_market, AmountType, LendingInstruction}, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, state::{ - BorrowLiquidityResult, LendingMarket, LiquidateObligationResult, - NewObligationCollateralParams, NewObligationLiquidityParams, NewObligationParams, + BorrowLiquidityResult, LendingMarket, LiquidateObligationResult, NewObligationParams, NewReserveParams, Obligation, ObligationCollateral, ObligationLiquidity, RepayLiquidityResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, - WithdrawCollateralResult, MAX_OBLIGATION_ACCOUNTS, PROGRAM_VERSION, + WithdrawCollateralResult, MAX_OBLIGATION_DATA, PROGRAM_VERSION, }, }; use flux_aggregator; @@ -27,9 +27,8 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; +use solana_sdk::account_info::next_account_infos; use spl_token::state::Account; -use std::convert::TryFrom; -use crate::state::NewReserveLiquidityParams; /// Processes an instruction pub fn process_instruction( @@ -64,6 +63,10 @@ pub fn process_instruction( msg!("Instruction: Init Reserve"); process_init_reserve(program_id, liquidity_amount, config, accounts) } + LendingInstruction::RefreshReserve => { + msg!("Instruction: Refresh Reserve"); + process_refresh_reserve(program_id, accounts) + } LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { msg!("Instruction: Deposit Reserve Liquidity"); process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts) @@ -72,10 +75,6 @@ pub fn process_instruction( msg!("Instruction: Redeem Reserve Collateral"); process_redeem_reserve_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::AccrueReserveInterest => { - msg!("Instruction: Accrue Reserve Interest"); - process_accrue_reserve_interest(program_id, accounts) - } LendingInstruction::InitObligation => { msg!("Instruction: Init Obligation"); process_init_obligation(program_id, accounts) @@ -84,14 +83,6 @@ pub fn process_instruction( msg!("Instruction: Refresh Obligation"); process_refresh_obligation(program_id, accounts) } - LendingInstruction::InitObligationCollateral => { - msg!("Instruction: Init Obligation Collateral"); - process_init_obligation_collateral(program_id, accounts) - } - LendingInstruction::RefreshObligationCollateral => { - msg!("Instruction: Refresh Obligation Collateral"); - process_refresh_obligation_collateral(program_id, accounts) - } LendingInstruction::DepositObligationCollateral { collateral_amount } => { msg!("Instruction: Deposit Obligation Collateral"); process_deposit_obligation_collateral(program_id, collateral_amount, accounts) @@ -108,14 +99,6 @@ pub fn process_instruction( accounts, ) } - LendingInstruction::InitObligationLiquidity => { - msg!("Instruction: Init Obligation Liquidity"); - process_init_obligation_liquidity(program_id, accounts) - } - LendingInstruction::RefreshObligationLiquidity => { - msg!("Instruction: Refresh Obligation Liquidity"); - process_refresh_obligation_liquidity(program_id, accounts) - } LendingInstruction::BorrowObligationLiquidity { liquidity_amount, liquidity_amount_type, @@ -282,6 +265,13 @@ fn process_init_reserve( assert_rent_exempt(rent, reserve_info)?; assert_uninitialized::(reserve_info)?; + // @TODO: check if valid + assert_uninitialized(reserve_liquidity_supply_info)?; + assert_uninitialized(reserve_collateral_mint_info)?; + assert_uninitialized(reserve_liquidity_fee_receiver_info)?; + assert_uninitialized(reserve_collateral_supply_info)?; + assert_uninitialized(destination_collateral_info)?; + if reserve_liquidity_supply_info.key == source_liquidity_info.key { msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); @@ -301,18 +291,19 @@ fn process_init_reserve( return Err(LendingError::InvalidSigner.into()); } - let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { - let aggregator_info = next_account_info(account_info_iter)?; + let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = + if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { + let aggregator_info = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, aggregator_info)?; + assert_rent_exempt(rent, aggregator_info)?; - // @TODO: is there a way to verify that aggregator_info uses the base and quote currency? - let answer = flux_aggregator::read_median(aggregator_info)?; + // @TODO: is there a way to verify that aggregator_info uses the base and quote currency? + let answer = flux_aggregator::read_median(aggregator_info)?; - (COption::Some(*aggregator_info.key), answer.median) - } else { - (COption::None, 1) - }; + (COption::Some(*aggregator_info.key), answer.median) + } else { + (COption::None, 1) + }; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -335,7 +326,7 @@ fn process_init_reserve( supply_pubkey: *reserve_liquidity_supply_info.key, fee_receiver: *reserve_liquidity_fee_receiver_info.key, aggregator: reserve_liquidity_aggregator, - median_price: reserve_liquidity_median_price + median_price: reserve_liquidity_median_price, }); let reserve_collateral = ReserveCollateral::new( *reserve_collateral_mint_info.key, @@ -412,27 +403,30 @@ fn process_init_reserve( Ok(()) } -// @TODO: support multiple reserves fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let reserve_info = next_account_info(accounts_iter)?; - let reserve_liquidity_aggregator_info = next_account_info(accounts_iter)?; + let account_info_iter = &mut accounts.iter().peekable(); let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &reserve.liquidity.aggregator != reserve_liquidity_aggregator_info.key { - msg!("Invalid reserve liquidity aggregator account"); - return Err(LendingError::InvalidAccountInput.into()); - } + while let Some(_) = account_info_iter.peek() { + let reserve_info = next_account_info(account_info_iter)?; + // @FIXME: handle optional aggregator + let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; - let answer = flux_aggregator::read_median(reserve_liquidity_aggregator_info)?; + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } + if &reserve.liquidity.aggregator != reserve_liquidity_aggregator_info.key { + msg!("Invalid reserve liquidity aggregator account"); + return Err(LendingError::InvalidAccountInput.into()); + } - reserve.liquidity.median_price = answer.median; - reserve.accrue_interest(clock.slot)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + let answer = flux_aggregator::read_median(reserve_liquidity_aggregator_info)?; + + reserve.liquidity.median_price = answer.median; + reserve.accrue_interest(clock.slot)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + } Ok(()) } @@ -619,24 +613,6 @@ fn process_redeem_reserve_collateral( Ok(()) } -// @FIMXE: remove, duplicated by refresh_reserve -#[inline(never)] // avoid stack frame limit -fn process_accrue_reserve_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - for reserve_info in account_info_iter { - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - - reserve.accrue_interest(clock.slot)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; - } - - Ok(()) -} - #[inline(never)] // avoid stack frame limit fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -667,7 +643,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro } fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; @@ -690,259 +666,69 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - let mut collateral_value = Decimal::zero(); - for pubkey in obligation.collateral { - let obligation_collateral_info = next_account_info(account_info_iter)?; - if obligation_collateral_info.owner != program_id { + // @FIXME: hashmap iteration order not guaranteed, iterate accounts instead + for (deposit_reserve_pubkey, collateral) in obligation.collateral.iter_mut() { + let deposit_reserve_info = next_account_info(account_info_iter)?; + if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if pubkey != obligation_collateral_info.key { - msg!("Invalid obligation collateral account"); + if deposit_reserve_pubkey != deposit_reserve_info.key { + msg!("Invalid deposit reserve"); return Err(LendingError::InvalidAccountInput.into()); } - let obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if &deposit_reserve.lending_market != lending_market_info.key { + msg!("Invalid deposit reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); + if deposit_reserve.last_update.is_stale(clock.slot)? { + return Err(LendingError::ReserveStale.into()); } - collateral_value = collateral_value.try_add(obligation_collateral.value)?; + collateral.market_value = deposit_reserve + .collateral_exchange_rate()? + .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? + .try_mul(deposit_reserve.liquidity.median_price)?; } - let mut liquidity_value = Decimal::zero(); - for pubkey in obligation.liquidity { - let obligation_liquidity_info = next_account_info(account_info_iter)?; - if obligation_liquidity_info.owner != program_id { + // @FIXME: hashmap iteration order not guaranteed, iterate accounts instead + for (borrow_reserve_pubkey, liquidity) in obligation.liquidity.iter_mut() { + let borrow_reserve_info = next_account_info(account_info_iter)?; + if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if pubkey != obligation_liquidity_info.key { - msg!("Invalid obligation liquidity account"); + if borrow_reserve_pubkey != borrow_reserve_info.key { + msg!("Invalid borrow reserve"); return Err(LendingError::InvalidAccountInput.into()); } - let obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if &borrow_reserve.lending_market != lending_market_info.key { + msg!("Invalid borrow reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); + if borrow_reserve.last_update.is_stale(clock.slot)? { + return Err(LendingError::ReserveStale.into()); } - liquidity_value = liquidity_value.try_add(obligation_liquidity.value)?; + liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; + liquidity.market_value = liquidity + .borrowed_amount_wads + .try_mul(borrow_reserve.liquidity.median_price)?; } - // @TODO: check this - if account_info_iter.count() > 0 { + if let Some(_) = account_info_iter.peek() { msg!("Too many obligation collateral or liquidity accounts"); return Err(LendingError::InvalidAccountInput.into()); } - obligation.collateral_value = collateral_value; - obligation.liquidity_value = liquidity_value; obligation.last_update.update_slot(clock.slot); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_init_obligation_collateral( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; - let obligation_token_owner_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; - let token_program_id = next_account_info(account_info_iter)?; - - assert_rent_exempt(rent, obligation_collateral_info)?; - assert_uninitialized::(obligation_collateral_info)?; - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? - if deposit_reserve.config.loan_to_value_ratio == 0 { - return Err(LendingError::ReserveCollateralDisabled.into()); - } - - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - // @FIXME: unchecked math - if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { - return Err(LendingError::ObligationAccountLimit.into()); - } - if obligation - .collateral - .contains(obligation_collateral_info.key) - { - return Err(LendingError::ObligationCollateralDuplicate.into()); - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - let obligation_collateral = ObligationCollateral::new(NewObligationCollateralParams { - current_slot: clock.slot, - obligation: *obligation_info.key, - deposit_reserve: *deposit_reserve_info.key, - token_mint: obligation_token_mint_info.key(), - }); - obligation.collateral.push(*obligation_collateral_info.key); - obligation.last_update.mark_stale(); - - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - spl_token_init_mint(TokenInitializeMintParams { - mint: obligation_token_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: deposit_reserve.liquidity.mint_decimals, - token_program: token_program_id.clone(), - })?; - - spl_token_init_account(TokenInitializeAccountParams { - account: obligation_token_output_info.clone(), - mint: obligation_token_mint_info.clone(), - owner: obligation_token_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - - Ok(()) -} - -fn process_refresh_obligation_collateral( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if deposit_reserve.dex_market.is_none() { - msg!("Deposit reserve must have a dex market"); - return Err(LendingError::InvalidAccountInput.into()); - } - if let COption::Some(dex_market_pubkey) = deposit_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - } - if deposit_reserve.last_update.is_stale(clock.slot) { - return Err(LendingError::ReserveStale.into()); - } - - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { - msg!("Invalid deposit reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - obligation_collateral.update_value( - deposit_reserve.collateral_exchange_rate()?, - token_converter, - &deposit_reserve.liquidity.mint_pubkey, - )?; - obligation_collateral.last_update.update_slot(clock.slot); - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; - // @TODO: should we mark the obligation stale here? - // could also iteratively update obligation.collateral_value - - Ok(()) -} - #[inline(never)] // avoid stack frame limit fn process_deposit_obligation_collateral( program_id: &Pubkey, @@ -958,12 +744,12 @@ fn process_deposit_obligation_collateral( let destination_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_output_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -990,6 +776,10 @@ fn process_deposit_obligation_collateral( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + // @TODO: is this necessary? we can set collateral.market_value if we have it though + if deposit_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? if deposit_reserve.config.loan_to_value_ratio == 0 { return Err(LendingError::ReserveCollateralDisabled.into()); @@ -1004,31 +794,6 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAccountInput.into()); } - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.deposit_reserve != deposit_reserve_info.key { - msg!("Invalid deposit reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); - } - if !obligation - .collateral - .contains(obligation_collateral_info.key) - { - msg!("Invalid obligation collateral account"); - return Err(LendingError::InvalidAccountInput.into()); - } - unpack_mint(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); @@ -1052,14 +817,16 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - obligation_collateral.deposit(collateral_amount)?; - obligation_collateral.last_update.mark_stale(); - obligation.last_update.mark_stale(); + let collateral = obligation + .collateral + .entry(*deposit_reserve_info.key) + .or_insert(ObligationCollateral::new()); + if obligation.collateral.len() + obligation.liquidity.len() > MAX_OBLIGATION_DATA { + return Err(LendingError::ObligationAccountLimit.into()); + } - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; + collateral.deposit(collateral_amount)?; + obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { @@ -1105,7 +872,6 @@ fn process_withdraw_obligation_collateral( let destination_collateral_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; let obligation_token_mint_info = next_account_info(account_info_iter)?; let obligation_token_input_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; @@ -1138,6 +904,9 @@ fn process_withdraw_obligation_collateral( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } + if withdraw_reserve.last_update.is_stale(clock.slot) { + return Err(LendingError::ReserveStale.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -1150,46 +919,18 @@ fn process_withdraw_obligation_collateral( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued if obligation.last_update < withdraw_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); + if obligation.collateral.len() == 0 { + return Err(LendingError::ObligationCollateralEmpty.into()); } - if !obligation + + let mut collateral = obligation .collateral - .contains(obligation_collateral_info.key) - { - msg!("Invalid obligation collateral account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_collateral.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation_collateral.deposited_amount == 0 { + .get(withdraw_reserve_info.key) + .ok_or(LendingError::InvalidObligationCollateral)?; + if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -1232,20 +973,14 @@ fn process_withdraw_obligation_collateral( } = obligation.withdraw_collateral( collateral_amount, collateral_amount_type, - &obligation_collateral, + &collateral, loan_to_value_ratio, obligation_token_mint.supply, )?; - // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? - obligation_collateral.withdraw(withdraw_amount)?; - obligation_collateral.last_update.mark_stale(); + // @FIXME: should withdraw_reserve.collateral.mint_total_supply change? + collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); - - ObligationCollateral::pack( - obligation_collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { @@ -1269,158 +1004,6 @@ fn process_withdraw_obligation_collateral( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_init_obligation_liquidity( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; - let token_program_id = next_account_info(account_info_iter)?; - - assert_rent_exempt(rent, obligation_liquidity_info)?; - assert_uninitialized::(obligation_liquidity_info)?; - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; - if obligation_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - // @FIXME: unchecked math - if obligation.collateral.len() + obligation.liquidity.len() + 1 > MAX_OBLIGATION_ACCOUNTS { - return Err(LendingError::ObligationAccountLimit.into()); - } - if obligation.liquidity.contains(obligation_liquidity_info.key) { - return Err(LendingError::ObligationLiquidityDuplicate.into()); - } - - let obligation_liquidity = ObligationLiquidity::new(NewObligationLiquidityParams { - current_slot: clock.slot, - obligation: *obligation_info.key, - borrow_reserve: *borrow_reserve_info.key, - }); - obligation.liquidity.push(*obligation_liquidity_info.key); - obligation.last_update.mark_stale(); - - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - Ok(()) -} - -fn process_refresh_obligation_liquidity( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_liquidity_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } - - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if borrow_reserve.dex_market.is_none() { - msg!("Borrow reserve must have a dex market"); - return Err(LendingError::InvalidAccountInput.into()); - } - if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - } - // @FIXME: borrow_reserve.cumulative_borrow_rate_wads is used below - // need to refresh reserve or restrict to current slot - if borrow_reserve.last_update.is_stale(clock.slot) { - return Err(LendingError::ReserveStale.into()); - } - - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { - msg!("Invalid borrow reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - - obligation_liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; - obligation_liquidity.update_value(token_converter, &borrow_reserve.liquidity.mint_pubkey)?; - obligation_liquidity.last_update.update_slot(clock.slot); - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; - // @TODO: should we mark the obligation stale here? - // could also iteratively update obligation.liquidity_value - - Ok(()) -} - #[inline(never)] // avoid stack frame limit fn process_borrow_obligation_liquidity( program_id: &Pubkey, @@ -1444,11 +1027,8 @@ fn process_borrow_obligation_liquidity( let borrow_reserve_info = next_account_info(account_info_iter)?; let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_orders_info = next_account_info(account_info_iter)?; let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1485,16 +1065,12 @@ fn process_borrow_obligation_liquidity( msg!("Invalid borrow reserve liquidity fee receiver account"); return Err(LendingError::InvalidAccountInput.into()); } - if borrow_reserve.dex_market.is_none() { + // @FIXME: not an issue if reserve currency is quote + if borrow_reserve.liquidity.aggregator.is_none() { + // @FIXME: error message msg!("Borrow reserve must have a dex market"); return Err(LendingError::InvalidAccountInput.into()); } - if let COption::Some(dex_market_pubkey) = borrow_reserve.dex_market { - if &dex_market_pubkey != dex_market_info.key { - msg!("Invalid dex market account"); - return Err(LendingError::InvalidAccountInput.into()); - } - } if borrow_reserve.last_update.is_stale(clock.slot) { return Err(LendingError::ReserveStale.into()); } @@ -1510,37 +1086,19 @@ fn process_borrow_obligation_liquidity( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued if obligation.last_update < borrow_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != borrow_reserve_info.key { - msg!("Invalid borrow reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - msg!("Invalid obligation liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); + if obligation.collateral.len() == 0 { + return Err(LendingError::ObligationCollateralEmpty.into()); } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); + + let liquidity = obligation + .liquidity + .entry(*borrow_reserve_info.key) + .or_insert(ObligationLiquidity::new()); + if obligation.collateral.len() + obligation.liquidity.len() > MAX_OBLIGATION_DATA { + return Err(LendingError::ObligationAccountLimit.into()); } let authority_signer_seeds = &[ @@ -1553,6 +1111,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } + // @TODO: consider moving let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; if loan_to_value > loan_to_value_ratio { @@ -1577,14 +1136,9 @@ fn process_borrow_obligation_liquidity( )?; borrow_reserve.liquidity.borrow(borrow_amount)?; - obligation_liquidity.borrow(borrow_amount)?; - obligation_liquidity.last_update.mark_stale(); + liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; @@ -1651,7 +1205,6 @@ fn process_repay_obligation_liquidity( let repay_reserve_info = next_account_info(account_info_iter)?; let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -1698,37 +1251,16 @@ fn process_repay_obligation_liquidity( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued if obligation.last_update < repay_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - msg!("Invalid obligation liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); + let mut liquidity = obligation + .liquidity + .get(repay_reserve_info.key) + .ok_or(LendingError::InvalidObligationLiquidity)?; + if liquidity.borrowed_amount_wads == Decimal::zero() { + return Err(LendingError::ObligationLiquidityEmpty.into()); } let authority_signer_seeds = &[ @@ -1744,21 +1276,12 @@ fn process_repay_obligation_liquidity( let RepayLiquidityResult { settle_amount, repay_amount, - } = repay_reserve.repay_liquidity( - liquidity_amount, - liquidity_amount_type, - &obligation_liquidity, - ); + } = repay_reserve.repay_liquidity(liquidity_amount, liquidity_amount_type, &liquidity); repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - obligation_liquidity.repay(settle_amount); - obligation_liquidity.last_update.mark_stale(); + liquidity.repay(settle_amount); obligation.last_update.mark_stale(); - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; @@ -1799,8 +1322,6 @@ fn process_liquidate_obligation( let withdraw_reserve_info = next_account_info(account_info_iter)?; let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_liquidity_info = next_account_info(account_info_iter)?; - let obligation_collateral_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -1859,8 +1380,8 @@ fn process_liquidate_obligation( return Err(LendingError::ReserveStale.into()); } - // @TODO: are these two checks necessary? - // is there a problem with repaying and receiving the same currency? + // @FIXME: what if a user borrows using the same reserve/mint for collateral? + // maybe liquidation should be allowed, but reduce the bonus if repay_reserve_info.key == withdraw_reserve_info.key { return Err(LendingError::DuplicateReserve.into()); } @@ -1879,8 +1400,6 @@ fn process_liquidate_obligation( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - // @TODO: is this enough? other reserves could have been updated that we don't check here, and - // they all affect the market value. need to think about when interest may be accrued if obligation.last_update < repay_reserve.last_update { return Err(LendingError::ObligationStale.into()); } @@ -1888,70 +1407,19 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let mut obligation_liquidity = - ObligationLiquidity::unpack(&obligation_liquidity_info.data.borrow())?; - if obligation_liquidity_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_liquidity.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_liquidity.borrow_reserve != repay_reserve_info.key { - msg!("Invalid repay reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if !obligation.liquidity.contains(obligation_liquidity_info.key) { - msg!("Invalid obligation liquidity account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_liquidity.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationLiquidityStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_liquidity.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation_liquidity.borrowed_amount_wads == Decimal::zero() { + let mut liquidity = obligation + .liquidity + .get_mut(repay_reserve.key) + .ok_or(LendingError::InvalidObligationLiquidity)?; + if liquidity.borrowed_amount_wads == Decimal::zero() { return Err(LendingError::ObligationLiquidityEmpty.into()); } - let mut obligation_collateral = - ObligationCollateral::unpack(&obligation_collateral_info.data.borrow())?; - if obligation_collateral_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &obligation_collateral.obligation != obligation_info.key { - msg!("Invalid obligation account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.deposit_reserve != withdraw_reserve_info.key { - msg!("Invalid withdraw reserve account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation_collateral.token_mint != obligation_token_mint_info.key { - msg!("Invalid obligation token mint"); - return Err(LendingError::InvalidTokenMint.into()); - } - if !obligation + let mut collateral = obligation .collateral - .contains(obligation_collateral_info.key) - { - msg!("Invalid obligation collateral account"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation_collateral.last_update.is_stale(clock.slot)? { - return Err(LendingError::ObligationCollateralStale.into()); - } - // @TODO: is this enough? other collateral/liquidity could have been updated that we don't - // check here. we could mark the obligation stale on every refresh of - // collateral/liquidity, but this means they can't be refreshed in parallel - if obligation.last_update < obligation_collateral.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation_collateral.deposited_amount == 0 { + .get_mut(withdraw_reserve_info.key) + .ok_or(LendingError::InvalidObligationCollateral)?; + if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -1965,6 +1433,7 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } + // @TODO: consider moving let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); let obligation_ltv = obligation.loan_to_value()?; if obligation_ltv < liquidation_threshold { @@ -1979,8 +1448,8 @@ fn process_liquidate_obligation( liquidity_amount, liquidity_amount_type, &obligation, - &obligation_liquidity, - &obligation_collateral, + &liquidity, + &collateral, )?; // @TODO: check if this can actually happen now @@ -1989,22 +1458,17 @@ fn process_liquidate_obligation( } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - obligation_liquidity.repay(settle_amount); - // @FIXME: shouldn't withdraw_reserve.collateral.mint_total_supply change? - obligation_collateral.withdraw(withdraw_amount); - obligation_liquidity.last_update.mark_stale(); - obligation_collateral.last_update.mark_stale(); + liquidity.repay(settle_amount); + // @FIXME: should withdraw_reserve.collateral.mint_total_supply change? + collateral.withdraw(withdraw_amount); obligation.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; ObligationCollateral::pack( - obligation_collateral, + collateral, &mut obligation_collateral_info.data.borrow_mut(), )?; - ObligationLiquidity::pack( - obligation_liquidity, - &mut obligation_liquidity_info.data.borrow_mut(), - )?; + ObligationLiquidity::pack(liquidity, &mut obligation_liquidity_info.data.borrow_mut())?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index 4f1c80dff44..5e4cbcdd04c 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -12,7 +12,7 @@ use solana_program::{ use std::cmp::Ordering; /// Number of slots to consider stale after -pub const STALE_AFTER_SLOTS: u64 = 10; +pub const STALE_AFTER_SLOTS: u64 = 0; /// Last update state #[derive(Clone, Debug, Default)] diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index b3e45a0c405..e5e74577809 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -3,15 +3,11 @@ mod last_update; mod lending_market; mod obligation; -mod obligation_collateral; -mod obligation_liquidity; mod reserve; pub use last_update::*; pub use lending_market::*; pub use obligation::*; -pub use obligation_collateral::*; -pub use obligation_liquidity::*; pub use reserve::*; use crate::math::{Decimal, WAD}; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 3bc29e8274a..695425478cd 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -12,29 +12,26 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; // @TODO: rename / relocate; true max is potentially 28 /// Max number of collateral and liquidity accounts combined for an obligation -pub const MAX_OBLIGATION_ACCOUNTS: usize = 10; +pub const MAX_OBLIGATION_DATA: usize = 10; /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] -pub struct Obligation { +pub struct Obligation<'a> { /// 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, - /// Collateral market value in quote currency - pub collateral_value: Decimal, - /// Liquidity market value in quote currency - pub liquidity_value: Decimal, - /// Collateral accounts for the obligation - pub collateral: Vec, - /// Liquidity accounts for the obligation - pub liquidity: Vec, + /// Collateral state for the obligation, keyed by deposit reserve address + pub collateral: HashMap, + /// Liquidity state for the obligation, keyed by borrow reserve address + pub liquidity: HashMap, } /// Create new obligation @@ -57,17 +54,25 @@ impl Obligation { version: PROGRAM_VERSION, last_update: LastUpdate::new(current_slot), lending_market, - collateral_value: Decimal::zero(), - liquidity_value: Decimal::zero(), - collateral: vec![], - liquidity: vec![], + collateral: HashMap::new(), + liquidity: HashMap::new(), } } - // @FIXME: error if collateral value is zero /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { - self.liquidity_value.try_div(self.collateral_value) + let mut liquidity_value = Decimal::zero(); + for (_, liquidity) in self.liquidity { + liquidity_value = liquidity_value.try_add(liquidity.market_value)?; + } + + let mut collateral_value = Decimal::zero(); + for (_, collateral) in self.collateral { + collateral_value = collateral_value.try_add(collateral.market_value)?; + } + + // @TODO: error if collateral value is zero? + liquidity_value.try_div(collateral_value) } pub fn withdraw_collateral( @@ -133,7 +138,112 @@ impl IsInitialized for Obligation { } } -const OBLIGATION_LEN: usize = 716; // 1 + 8 + 1 + 32 + 16 + 16 + 1 + 1 + (32 * 10) + (32 * 10) +/// Obligation collateral state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationCollateral { + /// 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() -> Self { + Self { + deposited_amount: 0, + // @TODO: should this be initialized with a real value on deposit? + 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(()) + } + + /// Amount of obligation tokens for given collateral + pub fn collateral_to_obligation_token_amount( + &self, + collateral_amount: u64, + obligation_token_supply: u64, + ) -> Result { + let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_amount)?; + withdraw_pct + .try_mul(obligation_token_supply)? + .try_floor_u64() + } +} + +/// Obligation liquidity state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ObligationLiquidity { + /// 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() -> Self { + Self { + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_amount_wads: Decimal::zero(), + // @TODO: should this be initialized with a real value on borrow? + 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: u64) -> ProgramResult { + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; + Ok(()) + } + + /// Accrue interest + pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { + if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { + return Err(LendingError::NegativeInterestRate.into()); + } + + 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 = 40; // 32 + 8 +const OBLIGATION_LIQUIDITY_LEN: usize = 64; // 32 + 16 + 16 +const OBLIGATION_LEN: usize = 716; // 1 + 8 + 1 + 32 + 16 + 16 + 1 + 1 + (64 * 10) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -145,49 +255,45 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, - collateral_value, - liquidity_value, - num_collateral, - num_liquidity, - accounts_flat, - _padding, + collateral_len, + liquidity_len, + data_flat, ) = mut_array_refs![ output, 1, 8, 1, PUBKEY_BYTES, - 16, - 16, 1, 1, - PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS, - PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS + OBLIGATION_LIQUIDITY_LEN * MAX_OBLIGATION_DATA ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_decimal(self.collateral_value, collateral_value); - pack_decimal(self.liquidity_value, liquidity_value); - - // @TODO: this seems clunky, is this correct? - *num_collateral = u8::try_from(self.collateral.len())?.to_le_bytes(); - *num_liquidity = u8::try_from(self.liquidity.len())?.to_le_bytes(); + *collateral_len = u8::try_from(self.collateral.len())?.to_le_bytes(); + *liquidity_len = u8::try_from(self.liquidity.len())?.to_le_bytes(); let mut offset = 0; - for pubkey in self.collateral.iter() { - let account = array_mut_ref![accounts_flat, offset, PUBKEY_BYTES]; - account.copy_from_slice(pubkey.as_ref()); - // @FIXME: unchecked math - offset += PUBKEY_BYTES; + for (deposit_reserve, collateral) in self.collateral.iter() { + let collateral_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (reserve, deposited_amount) = mut_array_refs![collateral_flat, PUBKEY_BYTES, 8]; + reserve.copy_from_slice(deposit_reserve.as_ref()); + *deposited_amount = collateral.deposited_amount.to_le_bytes(); + offset += OBLIGATION_COLLATERAL_LEN; } - for pubkey in self.liquidity.iter() { - let account = array_mut_ref![accounts_flat, offset, PUBKEY_BYTES]; - account.copy_from_slice(pubkey.as_ref()); - // @FIXME: unchecked math - offset += PUBKEY_BYTES; + for (borrow_reserve, liquidity) in self.liquidity.iter() { + let liquidity_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (reserve, cumulative_borrow_rate_wads, borrowed_amount_wads) = + mut_array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16]; + reserve.copy_from_slice(borrow_reserve.as_ref()); + *cumulative_borrow_rate_wads = liquidity.cumulative_borrow_rate_wads.to_le_bytes(); + *borrowed_amount_wads = liquidity.borrowed_amount_wads.to_le_bytes(); + offset += OBLIGATION_LIQUIDITY_LEN; } } @@ -199,47 +305,49 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, - collateral_value, - liquidity_value, - num_collateral, - num_liquidity, - accounts_flat, - _padding, + collateral_len, + liquidity_len, + data_flat, ) = array_refs![ input, 1, 8, 1, PUBKEY_BYTES, - 16, - 16, 1, 1, - PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS, - PUBKEY_BYTES * MAX_OBLIGATION_ACCOUNTS + OBLIGATION_LIQUIDITY_LEN * MAX_OBLIGATION_DATA ]; - let collateral_len = u8::from_le_bytes(*num_collateral); - let liquidity_len = u8::from_le_bytes(*num_liquidity); - // @FIXME: unchecked math - let total_len = collateral_len + liquidity_len; - - // @TODO: this seems clunky, is this correct? - let mut collateral = Vec::with_capacity(collateral_len.try_into()?); - let mut liquidity = Vec::with_capacity(liquidity_len.try_into()?); + let collateral_len = u8::from_le_bytes(*collateral_len); + let liquidity_len = u8::from_le_bytes(*liquidity_len); + let mut collateral = HashMap::with_capacity(usize::from(collateral_len)); + let mut liquidity = HashMap::with_capacity(usize::from(liquidity_len)); let mut offset = 0; - // @TODO: is there a more idiomatic/performant way to iterate this? - for account in accounts_flat.chunks(PUBKEY_BYTES) { - if offset < collateral_len { - collateral.push(Pubkey::new(account)); - } else if offset < total_len { - liquidity.push(Pubkey::new(account)); - } else { - break; - } - // @FIXME: unchecked math - offset += 1; + for _ in collateral_len { + let collateral_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + let (deposit_reserve, deposited_amount) = array_refs![collateral_flat, PUBKEY_BYTES, 8]; + collateral.insert( + Pubkey::new(deposit_reserve), + ObligationCollateral { + deposited_amount: u64::from_le_bytes(*deposited_amount), + }, + ); + offset += OBLIGATION_COLLATERAL_LEN; + } + for _ in liquidity_len { + let liquidity_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; + let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads) = + array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16]; + liquidity.insert( + Pubkey::new(borrow_reserve), + ObligationLiquidity { + cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), + borrowed_amount_wads: unpack_decimal(borrowed_amount_wads), + }, + ); + offset += OBLIGATION_LIQUIDITY_LEN; } Ok(Self { @@ -249,164 +357,8 @@ impl Pack for Obligation { stale: bool::from(u8::from_le_bytes(*last_update_stale)), }, lending_market: Pubkey::new_from_array(*lending_market), - collateral_value: unpack_decimal(collateral_value), - liquidity_value: unpack_decimal(liquidity_value), collateral, liquidity, }) } } - -// @FIXME: tests -#[cfg(test)] -mod test { - use super::*; - use crate::math::TryAdd; - use proptest::prelude::*; - - const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000% - - #[test] - fn obligation_accrue_interest_failure() { - assert_eq!( - Obligation { - cumulative_borrow_rate_wads: Decimal::zero(), - ..Obligation::default() - } - .accrue_interest(Decimal::one()), - Err(LendingError::MathOverflow.into()) - ); - - assert_eq!( - Obligation { - cumulative_borrow_rate_wads: Decimal::from(2u64), - ..Obligation::default() - } - .accrue_interest(Decimal::one()), - Err(LendingError::NegativeInterestRate.into()) - ); - - assert_eq!( - Obligation { - cumulative_borrow_rate_wads: Decimal::one(), - borrowed_wads: Decimal::from(u64::MAX), - ..Obligation::default() - } - .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), - Err(LendingError::MathOverflow.into()) - ); - } - - // Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1 - prop_compose! { - fn cumulative_rates()(rate in 1..=u128::MAX)( - current_rate in Just(rate), - max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128) - ) -> (u128, u128) { - (current_rate, max_new_rate) - } - } - - const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128; - - // Creates liquidity amounts (repay, borrow) where repay < borrow - prop_compose! { - fn repay_partial_amounts()(repay in 1..=u64::MAX)( - liquidity_amount in Just(repay), - borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED - ) -> (u64, u128) { - (liquidity_amount, borrowed_liquidity) - } - } - - // Creates liquidity amounts (repay, borrow) where repay >= borrow - prop_compose! { - fn repay_full_amounts()(repay in 1..=u64::MAX)( - liquidity_amount in Just(repay), - borrowed_liquidity in 0..=(WAD as u128 * repay as u128) - ) -> (u64, u128) { - (liquidity_amount, borrowed_liquidity) - } - } - - // Creates collateral amounts (collateral, obligation tokens) where c <= ot - prop_compose! { - fn collateral_amounts()(collateral in 1..=u64::MAX)( - deposited_collateral_tokens in Just(collateral), - obligation_tokens in collateral..=u64::MAX - ) -> (u64, u64) { - (deposited_collateral_tokens, obligation_tokens) - } - } - - proptest! { - #[test] - fn repay_partial( - (liquidity_amount, borrowed_liquidity) in repay_partial_amounts(), - (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), - ) { - let borrowed_wads = Decimal::from_scaled_val(borrowed_liquidity); - let mut state = Obligation { - borrowed_wads, - deposited_collateral_tokens, - ..Obligation::default() - }; - - let repay_result = state.repay(liquidity_amount, obligation_tokens)?; - assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); - assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens); - assert!(repay_result.obligation_token_amount < obligation_tokens); - assert!(state.borrowed_wads < borrowed_wads); - assert!(state.borrowed_wads > Decimal::zero()); - assert!(state.deposited_collateral_tokens > 0); - - let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?; - let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?; - assert!(obligation_token_rate <= collateral_withdraw_rate); - } - - #[test] - fn repay_full( - (liquidity_amount, borrowed_liquidity) in repay_full_amounts(), - (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), - ) { - let borrowed_wads = Decimal::from_scaled_val(borrowed_liquidity); - let mut state = Obligation { - borrowed_wads, - deposited_collateral_tokens, - ..Obligation::default() - }; - - let repay_result = state.repay(liquidity_amount, obligation_tokens)?; - assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); - assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens); - assert_eq!(repay_result.obligation_token_amount, obligation_tokens); - assert_eq!(repay_result.decimal_repay_amount, borrowed_wads); - assert_eq!(state.borrowed_wads, Decimal::zero()); - assert_eq!(state.deposited_collateral_tokens, 0); - } - - #[test] - fn accrue_interest( - borrowed_liquidity in 0..=u64::MAX, - (current_borrow_rate, new_borrow_rate) in cumulative_rates(), - ) { - let borrowed_wads = Decimal::from(borrowed_liquidity); - let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; - let mut state = Obligation { - borrowed_wads, - cumulative_borrow_rate_wads, - ..Obligation::default() - }; - - let next_cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; - state.accrue_interest(next_cumulative_borrow_rate_wads)?; - - if next_cumulative_borrow_rate_wads > cumulative_borrow_rate_wads { - assert!(state.borrowed_wads > borrowed_wads); - } else { - assert!(state.borrowed_wads == borrowed_wads); - } - } - } -} diff --git a/token-lending/program/src/state/obligation_collateral.rs b/token-lending/program/src/state/obligation_collateral.rs deleted file mode 100644 index 7bbab6a5ab2..00000000000 --- a/token-lending/program/src/state/obligation_collateral.rs +++ /dev/null @@ -1,177 +0,0 @@ -use super::*; -use crate::{ - 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, - program_error::ProgramError, - program_pack::{IsInitialized, Pack, Sealed}, - pubkey::{Pubkey, PUBKEY_BYTES}, -}; -use std::convert::TryInto; - -/// Obligation collateral state -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ObligationCollateral { - /// Version of the obligation collateral - pub version: u8, - /// Last update to deposited tokens or their market value - pub last_update: LastUpdate, - /// Obligation the collateral is associated with - pub obligation: Pubkey, - /// Reserve which collateral tokens were deposited into - pub deposit_reserve: Pubkey, - /// Mint address of the tokens for this obligation collateral - pub token_mint: Pubkey, - /// Amount of collateral deposited - pub deposited_amount: u64, - /// Market value of collateral in quote currency - pub value: Decimal, -} - -/// Create new obligation collateral -pub struct NewObligationCollateralParams { - /// Current slot - pub current_slot: Slot, - /// Obligation address - pub obligation: Pubkey, - /// Deposit reserve address - pub deposit_reserve: Pubkey, - /// Obligation token mint address - pub token_mint: Pubkey, -} - -impl ObligationCollateral { - /// Create new obligation collateral - pub fn new(params: NewObligationCollateralParams) -> Self { - let NewObligationCollateralParams { - current_slot, - obligation, - deposit_reserve, - token_mint, - } = params; - - Self { - version: PROGRAM_VERSION, - last_update: LastUpdate::new(current_slot), - obligation, - deposit_reserve, - token_mint, - deposited_amount: 0, - 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(()) - } - - /// Update market value of collateral - pub fn update_value( - &mut self, - collateral_exchange_rate: CollateralExchangeRate, - token_converter: impl TokenConverter, - liquidity_token_mint: &Pubkey, - ) -> ProgramResult { - let liquidity_amount = collateral_exchange_rate - .decimal_collateral_to_liquidity(self.deposited_amount.into())?; - // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.value = token_converter.convert(liquidity_amount, liquidity_token_mint)?; - Ok(()) - } - - /// Amount of obligation tokens for given collateral - pub fn collateral_to_obligation_token_amount( - &self, - collateral_amount: u64, - obligation_token_supply: u64, - ) -> Result { - let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_amount)?; - withdraw_pct - .try_mul(obligation_token_supply)? - .try_floor_u64() - } -} - -impl Sealed for ObligationCollateral {} -impl IsInitialized for ObligationCollateral { - fn is_initialized(&self) -> bool { - self.version != UNINITIALIZED_VERSION - } -} - -const OBLIGATION_COLLATERAL_LEN: usize = 258; // 1 + 8 + 1 + 32 + 32 + 32 + 8 + 16 + 128 -impl Pack for ObligationCollateral { - const LEN: usize = OBLIGATION_COLLATERAL_LEN; - - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, OBLIGATION_COLLATERAL_LEN]; - let ( - version, - last_update_slot, - last_update_stale, - obligation, - deposit_reserve, - token_mint, - deposited_amount, - value, - _padding, - ) = mut_array_refs![output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16, 128]; - - *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update.slot.to_le_bytes(); - *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); - obligation.copy_from_slice(self.obligation.as_ref()); - deposit_reserve.copy_from_slice(self.deposit_reserve.as_ref()); - token_mint.copy_from_slice(self.token_mint.as_ref()); - *deposited_amount = self.deposited_amount.to_le_bytes(); - pack_decimal(self.value, value); - } - - /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). - fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, OBLIGATION_COLLATERAL_LEN]; - #[allow(clippy::ptr_offset_with_cast)] - let ( - version, - last_update_slot, - last_update_stale, - obligation, - deposit_reserve, - token_mint, - deposited_amount, - value, - _padding, - ) = array_refs![input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16, 128]; - - Ok(Self { - version: u8::from_le_bytes(*version), - last_update: LastUpdate { - slot: u64::from_le_bytes(*last_update_slot), - stale: bool::from(u8::from_le_bytes(*last_update_stale)), - }, - obligation: Pubkey::new_from_array(*obligation), - deposit_reserve: Pubkey::new_from_array(*deposit_reserve), - token_mint: Pubkey::new_from_array(*token_mint), - deposited_amount: u64::from_le_bytes(*deposited_amount), - value: unpack_decimal(value), - }) - } -} diff --git a/token-lending/program/src/state/obligation_liquidity.rs b/token-lending/program/src/state/obligation_liquidity.rs deleted file mode 100644 index b0280346c10..00000000000 --- a/token-lending/program/src/state/obligation_liquidity.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::*; -use crate::{ - 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, - program_error::ProgramError, - program_pack::{IsInitialized, Pack, Sealed}, - pubkey::{Pubkey, PUBKEY_BYTES}, -}; -use std::convert::TryInto; - -/// Obligation liquidity state -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ObligationLiquidity { - /// Version of the obligation liquidity - pub version: u8, - /// Last update to accrued interest, borrowed wads, or their market value - pub last_update: LastUpdate, - /// Obligation the liquidity is associated with - pub obligation: Pubkey, - /// Reserve which liquidity tokens were 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, - /// Market value of liquidity in quote currency - pub value: Decimal, -} - -/// Create new obligation liquidity -pub struct NewObligationLiquidityParams { - /// Current slot - pub current_slot: Slot, - /// Obligation address - pub obligation: Pubkey, - /// Borrow reserve address - pub borrow_reserve: Pubkey, -} - -impl ObligationLiquidity { - /// Create new obligation liquidity - pub fn new(params: NewObligationLiquidityParams) -> Self { - let NewObligationLiquidityParams { - current_slot, - obligation, - borrow_reserve, - } = params; - - Self { - version: PROGRAM_VERSION, - last_update: LastUpdate::new(current_slot), - obligation, - borrow_reserve, - cumulative_borrow_rate_wads: Decimal::one(), - borrowed_amount_wads: Decimal::zero(), - 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: u64) -> ProgramResult { - self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; - Ok(()) - } - - /// Accrue interest - pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { - if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { - return Err(LendingError::NegativeInterestRate.into()); - } - - 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(()) - } - - /// Update market value of liquidity - pub fn update_value( - &mut self, - token_converter: impl TokenConverter, - from_token_mint: &Pubkey, - ) -> ProgramResult { - // @TODO: this may be slow/inaccurate for large amounts depending on dex market - self.value = token_converter.convert(self.borrowed_amount_wads, from_token_mint)?; - Ok(()) - } -} - -impl Sealed for ObligationLiquidity {} -impl IsInitialized for ObligationLiquidity { - fn is_initialized(&self) -> bool { - self.version != UNINITIALIZED_VERSION - } -} - -const OBLIGATION_LIQUIDITY_LEN: usize = 250; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 128 -impl Pack for ObligationLiquidity { - const LEN: usize = OBLIGATION_LIQUIDITY_LEN; - - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, OBLIGATION_LIQUIDITY_LEN]; - let ( - version, - last_update_slot, - last_update_stale, - obligation, - borrow_reserve, - cumulative_borrow_rate_wads, - borrowed_wads, - value, - _padding, - ) = mut_array_refs![output, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 16, 16, 16, 128]; - - *version = self.version.to_le_bytes(); - *last_update_slot = self.last_update.slot.to_le_bytes(); - *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); - obligation.copy_from_slice(self.obligation.as_ref()); - borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref()); - pack_decimal( - self.cumulative_borrow_rate_wads, - cumulative_borrow_rate_wads, - ); - pack_decimal(self.borrowed_amount_wads, borrowed_wads); - pack_decimal(self.value, value); - } - - /// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). - fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, OBLIGATION_LIQUIDITY_LEN]; - #[allow(clippy::ptr_offset_with_cast)] - let ( - version, - last_update_slot, - last_update_stale, - obligation, - borrow_reserve, - cumulative_borrow_rate_wads, - borrowed_wads, - value, - _padding, - ) = array_refs![input, 1, 8, 1, PUBKEY_BYTES, PUBKEY_BYTES, 16, 16, 16, 128]; - - Ok(Self { - version: u8::from_le_bytes(*version), - last_update: LastUpdate { - slot: u64::from_le_bytes(*last_update_slot), - stale: bool::from(u8::from_le_bytes(*last_update_stale)), - }, - obligation: Pubkey::new_from_array(*obligation), - borrow_reserve: Pubkey::new_from_array(*borrow_reserve), - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), - borrowed_amount_wads: unpack_decimal(borrowed_wads), - value: unpack_decimal(value), - }) - } -} diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 022f12dfc66..3a376e661e8 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -82,8 +82,6 @@ impl Reserve { let collateral_exchange_rate = self.collateral_exchange_rate()?; let liquidity_amount = collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?; - // @FIXME: collateral represents a share of available liquidity, - // it should never be more than what's available if liquidity_amount > self.liquidity.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } @@ -401,7 +399,7 @@ pub struct ReserveLiquidity { pub aggregator: COption, /// Reserve liquidity cumulative borrow rate pub cumulative_borrow_rate_wads: Decimal, - /// Reserve liquidity median price + /// Reserve liquidity median price in quote currency pub median_price: u64, /// Reserve liquidity available pub available_amount: u64, @@ -869,617 +867,4 @@ impl Pack for Reserve { }, }) } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::math::{PERCENT_SCALER, WAD}; - use proptest::prelude::*; - use std::cmp::Ordering; - - const MAX_LIQUIDITY: u64 = u64::MAX / 5; - - struct MockConverter(Decimal); - impl TokenConverter for MockConverter { - fn best_price(&mut self, _token_mint: &Pubkey) -> Result { - Ok(self.0) - } - - fn convert( - self, - from_amount: Decimal, - _from_token_mint: &Pubkey, - ) -> Result { - from_amount.try_mul(self.0) - } - } - - /// Convert reserve liquidity tokens to the collateral tokens of another reserve - fn liquidity_in_other_collateral( - liquidity_amount: u64, - collateral_exchange_rate: CollateralExchangeRate, - conversion_rate: Decimal, - ) -> Result { - collateral_exchange_rate.decimal_liquidity_to_collateral( - Decimal::from(liquidity_amount).try_mul(conversion_rate)?, - ) - } - - // Creates rates (min, opt, max) where 0 <= min <= opt <= max <= MAX - prop_compose! { - fn borrow_rates()(optimal_rate in 0..=u8::MAX)( - min_rate in 0..=optimal_rate, - optimal_rate in Just(optimal_rate), - max_rate in optimal_rate..=u8::MAX, - ) -> (u8, u8, u8) { - (min_rate, optimal_rate, max_rate) - } - } - - // Creates rates (threshold, ltv) where 2 <= threshold <= 100 and threshold <= ltv <= 1,000% - prop_compose! { - fn unhealthy_rates()(threshold in 2..=100u8)( - ltv_rate in threshold as u64..=1000u64, - threshold in Just(threshold), - ) -> (Decimal, u8) { - (Decimal::from_scaled_val(ltv_rate as u128 * PERCENT_SCALER as u128), threshold) - } - } - - // Creates a range of reasonable token conversion rates - prop_compose! { - fn token_conversion_rate()( - conversion_rate in 1..=u16::MAX, - invert_conversion_rate: bool, - ) -> Decimal { - let conversion_rate = Decimal::from(conversion_rate as u64); - if invert_conversion_rate { - Decimal::one().try_div(conversion_rate).unwrap() - } else { - conversion_rate - } - } - } - - // Creates a range of reasonable collateral exchange rates - prop_compose! { - fn collateral_exchange_rate_range()(percent in 1..=500u64) -> CollateralExchangeRate { - CollateralExchangeRate(Rate::from_scaled_val(percent * PERCENT_SCALER)) - } - } - - proptest! { - #[test] - fn unhealthy_obligations_can_be_liquidated( - obligation_collateral in 1..=u64::MAX, - (obligation_ltv, liquidation_threshold) in unhealthy_rates(), - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in token_conversion_rate(), - ) { - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold, - ..ReserveConfig::default() - }; - - // Create unhealthy obligation at target LTV - let collateral_value = collateral_exchange_rate - .decimal_collateral_to_liquidity(Decimal::from(obligation_collateral as u64))? - .try_div(token_conversion_rate)?; - - // Ensure that collateral value fits in u64 - prop_assume!(collateral_value.try_round_u64().is_ok()); - - let borrowed_liquidity_wads = collateral_value - .try_mul(obligation_ltv)? - .try_add(Decimal::from_scaled_val(1u128))? // ensure loan is unhealthy - .max(Decimal::from(2u64)); // avoid dust account closure - - // Ensure that borrow value fits in u64 - prop_assume!(borrowed_liquidity_wads.try_round_u64().is_ok()); - - let obligation = Obligation { - deposited_collateral_tokens: obligation_collateral as u64, - borrowed_liquidity_wads, - ..Obligation::default() - }; - - // Ensure that the token conversion fits in a Decimal - { - let token_converter = MockConverter(token_conversion_rate); - let decimal_repay_amount = Decimal::from(obligation.max_liquidation_amount()?); - // Calculate the amount of collateral that will be received - let receive_liquidity_amount_result = - token_converter.convert(decimal_repay_amount, &Pubkey::default()); - prop_assume!(receive_liquidity_amount_result.is_ok()); - } - - // Liquidate with max amount to ensure obligation can be liquidated - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - u64::MAX, - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(token_conversion_rate) - ); - - let liquidate_result = liquidate_result.unwrap(); - let expected_withdraw_amount = liquidity_in_other_collateral( - liquidate_result.repay_amount, - collateral_exchange_rate, - token_conversion_rate, - )?.min(obligation.deposited_collateral_tokens.into()); - - assert!(liquidate_result.repay_amount > 0); - assert!(liquidate_result.withdraw_amount > 0); - - let min_withdraw_amount = expected_withdraw_amount.try_floor_u64()?; - let max_withdraw_amount = expected_withdraw_amount.try_ceil_u64()?; - let max_repay_amount = obligation.borrowed_liquidity_wads - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .try_ceil_u64()?; - - assert!(liquidate_result.withdraw_amount >= min_withdraw_amount); - assert!(liquidate_result.withdraw_amount <= max_withdraw_amount); - assert!(liquidate_result.repay_amount <= max_repay_amount); - - let defaulted = liquidate_result.withdraw_amount == obligation.deposited_collateral_tokens; - if defaulted { - assert_eq!(liquidate_result.settle_amount, borrowed_liquidity_wads); - assert!(liquidate_result.repay_amount < liquidate_result.settle_amount.try_floor_u64()?); - } else { - assert_eq!(liquidate_result.settle_amount.try_ceil_u64()?, liquidate_result.repay_amount); - assert!(liquidate_result.withdraw_amount < obligation.deposited_collateral_tokens); - } - } - - #[test] - fn current_borrow_rate( - total_liquidity in 0..=MAX_LIQUIDITY, - borrowed_percent in 0..=WAD, - optimal_utilization_rate in 0..=100u8, - (min_borrow_rate, optimal_borrow_rate, max_borrow_rate) in borrow_rates(), - ) { - let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; - let reserve = Reserve { - liquidity: ReserveLiquidity { - borrowed_amount_wads, - available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - min_borrow_rate, - optimal_borrow_rate, - max_borrow_rate, - optimal_utilization_rate, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let current_borrow_rate = reserve.current_borrow_rate()?; - assert!(current_borrow_rate >= Rate::from_percent(min_borrow_rate)); - assert!(current_borrow_rate <= Rate::from_percent(max_borrow_rate)); - - let optimal_borrow_rate = Rate::from_percent(optimal_borrow_rate); - let current_rate = reserve.liquidity.utilization_rate()?; - match current_rate.cmp(&Rate::from_percent(optimal_utilization_rate)) { - Ordering::Less => { - if min_borrow_rate == reserve.config.optimal_borrow_rate { - assert_eq!(current_borrow_rate, optimal_borrow_rate); - } else { - assert!(current_borrow_rate < optimal_borrow_rate); - } - } - Ordering::Equal => assert!(current_borrow_rate == optimal_borrow_rate), - Ordering::Greater => { - if max_borrow_rate == reserve.config.optimal_borrow_rate { - assert_eq!(current_borrow_rate, optimal_borrow_rate); - } else { - assert!(current_borrow_rate > optimal_borrow_rate); - } - } - } - } - - #[test] - fn allowed_borrow_for_collateral( - collateral_amount in 0..=u32::MAX as u64, - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in 1..=u64::MAX, - loan_to_value_ratio in 1..100u8, - ) { - let total_liquidity = 1_000_000; - let collateral_token_supply = collateral_exchange_rate - .liquidity_to_collateral(total_liquidity)?; - let reserve = Reserve { - collateral: ReserveCollateral { - mint_total_supply: collateral_token_supply, - ..ReserveCollateral::default() - }, - liquidity: ReserveLiquidity { - available_amount: total_liquidity, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - loan_to_value_ratio, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); - let borrow_amount = reserve.allowed_borrow_for_collateral( - collateral_amount, - MockConverter(conversion_rate) - )?; - - // Allowed borrow should be conservatively low and therefore slightly less or equal - // to the precise equivalent for the given collateral. When it is converted back - // to collateral, the returned value should be equal or lower than the original - // collateral amount. - let collateral_amount_lower = reserve.required_collateral_for_borrow( - borrow_amount, - &Pubkey::default(), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // After incrementing the allowed borrow, its value should be slightly more or equal - // to the precise equivalent of the original collateral. When it is converted back - // to collateral, the returned value should be equal or higher than the original - // collateral amount since required collateral should be conservatively high. - let collateral_amount_upper = reserve.required_collateral_for_borrow( - borrow_amount + 1, - &Pubkey::default(), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // Assert that reversing the calculation returns approx original amount - assert!(collateral_amount >= collateral_amount_lower); - assert!(collateral_amount <= collateral_amount_upper); - } - - #[test] - fn required_collateral_for_borrow( - borrow_amount in 0..=u32::MAX as u64, - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in 1..=u64::MAX, - loan_to_value_ratio in 1..=100u8, - ) { - let total_liquidity = 1_000_000; - let collateral_token_supply = collateral_exchange_rate - .liquidity_to_collateral(total_liquidity)?; - let reserve = Reserve { - collateral: ReserveCollateral { - mint_total_supply: collateral_token_supply, - ..ReserveCollateral::default() - }, - liquidity: ReserveLiquidity { - available_amount: total_liquidity, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - loan_to_value_ratio, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); - let collateral_amount = reserve.required_collateral_for_borrow( - borrow_amount, - &Pubkey::default(), - MockConverter(conversion_rate) - )?; - - // Required collateral should be conservatively high and therefore slightly more or equal - // to the precise equivalent for the desired borrow amount. When it is converted back - // to borrow, the returned value should be equal or higher than the original - // borrow amount. - let borrow_amount_upper = reserve.allowed_borrow_for_collateral( - collateral_amount, - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // After decrementing the required collateral, its value should be slightly less or equal - // to the precise equivalent of the original borrow. When it is converted back - // to borrow, the returned value should be equal or lower than the original - // borrow amount since allowed borrow should be conservatively low. - let borrow_amount_lower = reserve.allowed_borrow_for_collateral( - collateral_amount.saturating_sub(1), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // Assert that reversing the calculation returns approx original amount - assert!(borrow_amount >= borrow_amount_lower); - assert!(borrow_amount <= borrow_amount_upper); - } - - #[test] - fn current_utilization_rate( - total_liquidity in 0..=MAX_LIQUIDITY, - borrowed_percent in 0..=WAD, - ) { - let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; - let liquidity = ReserveLiquidity { - borrowed_amount_wads, - available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?, - ..ReserveLiquidity::default() - }; - - let current_rate = liquidity.utilization_rate()?; - assert!(current_rate <= Rate::one()); - } - - #[test] - fn collateral_exchange_rate( - total_liquidity in 0..=MAX_LIQUIDITY, - borrowed_percent in 0..=WAD, - collateral_multiplier in 0..=(5*WAD), - borrow_rate in 0..=u8::MAX, - ) { - let borrowed_liquidity_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; - let available_liquidity = total_liquidity - borrowed_liquidity_wads.try_round_u64()?; - let mint_total_supply = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(collateral_multiplier))?.try_round_u64()?; - - let mut reserve = Reserve { - collateral: ReserveCollateral { - mint_total_supply, - ..ReserveCollateral::default() - }, - liquidity: ReserveLiquidity { - borrowed_amount_wads: borrowed_liquidity_wads, - available_amount: available_liquidity, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - min_borrow_rate: borrow_rate, - optimal_borrow_rate: borrow_rate, - optimal_utilization_rate: 100, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let exchange_rate = reserve.collateral_exchange_rate()?; - assert!(exchange_rate.0.to_scaled_val() <= 5u128 * WAD as u128); - - // After interest accrual, total liquidity increases and collateral are worth more - reserve.accrue_interest(1)?; - - let new_exchange_rate = reserve.collateral_exchange_rate()?; - if borrow_rate > 0 && total_liquidity > 0 && borrowed_percent > 0 { - assert!(new_exchange_rate.0 < exchange_rate.0); - } else { - assert_eq!(new_exchange_rate.0, exchange_rate.0); - } - } - - #[test] - fn compound_interest( - slots_elapsed in 0..=SLOTS_PER_YEAR, - borrow_rate in 0..=u8::MAX, - ) { - let mut reserve = Reserve::default(); - let borrow_rate = Rate::from_percent(borrow_rate); - - // Simulate running for max 1000 years, assuming that interest is - // compounded at least once a year - for _ in 0..1000 { - reserve.compound_interest(borrow_rate, slots_elapsed)?; - reserve.cumulative_borrow_rate_wads.to_scaled_val()?; - } - } - - #[test] - fn reserve_accrue_interest( - slots_elapsed in 0..=SLOTS_PER_YEAR, - borrowed_liquidity in 0..=u64::MAX, - borrow_rate in 0..=u8::MAX, - ) { - let borrowed_amount_wads = Decimal::from(borrowed_liquidity); - let mut reserve = Reserve { - liquidity: ReserveLiquidity { - borrowed_amount_wads, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - max_borrow_rate: borrow_rate, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - reserve.accrue_interest(slots_elapsed)?; - - if borrow_rate > 0 && slots_elapsed > 0 { - assert!(reserve.liquidity.borrowed_amount_wads > borrowed_amount_wads); - } else { - assert!(reserve.liquidity.borrowed_amount_wads == borrowed_amount_wads); - } - } - - #[test] - fn borrow_fee_calculation( - borrow_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails - host_fee_percentage in 0..=100u8, - borrow_amount in 3..=u64::MAX, // start at 3 to ensure calculation success - // 0, 1, and 2 are covered in the minimum tests - ) { - let fees = ReserveFees { - borrow_fee_wad, - host_fee_percentage, - }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(borrow_amount)?; - - // The total fee can't be greater than the amount borrowed, as long - // as amount borrowed is greater than 2. - // At a borrow amount of 2, we can get a total fee of 2 if a host - // fee is also specified. - assert!(total_fee <= borrow_amount); - - // the host fee can't be greater than the total fee - assert!(host_fee <= total_fee); - - // for all fee rates greater than 0, we must have some fee - if borrow_fee_wad > 0 { - assert!(total_fee > 0); - } - - if host_fee_percentage == 100 { - // if the host fee percentage is maxed at 100%, it should get all the fee - assert_eq!(host_fee, total_fee); - } - - // if there's a host fee and some borrow fee, host fee must be greater than 0 - if host_fee_percentage > 0 && borrow_fee_wad > 0 { - assert!(host_fee > 0); - } else { - assert_eq!(host_fee, 0); - } - } - } - - #[test] - fn liquidate_amount_too_small() { - let conversion_rate = Decimal::from_scaled_val(PERCENT_SCALER as u128); // 1% - let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold: 80u8, - liquidation_bonus: 5u8, - ..ReserveConfig::default() - }; - - let obligation = Obligation { - deposited_collateral_tokens: 1, - borrowed_liquidity_wads: Decimal::from(100u64), - ..Obligation::default() - }; - - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - 1u64, // converts to 0.01 collateral - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(conversion_rate), - ); - - assert_eq!( - liquidate_result.unwrap_err(), - LendingError::LiquidationTooSmall.into() - ); - } - - #[test] - fn liquidate_dust_obligation() { - let conversion_rate = Decimal::one(); - let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold: 80u8, - liquidation_bonus: 5u8, - ..ReserveConfig::default() - }; - - let obligation = Obligation { - deposited_collateral_tokens: 1, - borrowed_liquidity_wads: Decimal::one() - .try_add(Decimal::from_scaled_val(1u128)) - .unwrap(), - ..Obligation::default() - }; - - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - 2, - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(conversion_rate), - ) - .unwrap(); - - assert_eq!( - liquidate_result.repay_amount, - obligation.borrowed_liquidity_wads.try_ceil_u64().unwrap() - ); - assert_eq!( - liquidate_result.withdraw_amount, - obligation.deposited_collateral_tokens - ); - assert_eq!( - liquidate_result.settle_amount, - obligation.borrowed_liquidity_wads - ); - } - - #[test] - fn borrow_fee_calculation_min_host() { - let fees = ReserveFees { - borrow_fee_wad: 10_000_000_000_000_000, // 1% - host_fee_percentage: 20, - }; - - // only 2 tokens borrowed, get error - let err = fees.calculate_borrow_fees(2).unwrap_err(); - assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 3 tokens - - // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(1).unwrap_err(); - assert_eq!(err, LendingError::BorrowTooSmall.into()); - - // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); - assert_eq!(total_fee, 0); - assert_eq!(host_fee, 0); - } - - #[test] - fn borrow_fee_calculation_min_no_host() { - let fees = ReserveFees { - borrow_fee_wad: 10_000_000_000_000_000, // 1% - host_fee_percentage: 0, - }; - - // only 2 tokens borrowed, ok - let (total_fee, host_fee) = fees.calculate_borrow_fees(2).unwrap(); - assert_eq!(total_fee, 1); - assert_eq!(host_fee, 0); - - // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(1).unwrap_err(); - assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 2 tokens - - // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); - assert_eq!(total_fee, 0); - assert_eq!(host_fee, 0); - } - - #[test] - fn borrow_fee_calculation_host() { - let fees = ReserveFees { - borrow_fee_wad: 10_000_000_000_000_000, // 1% - host_fee_percentage: 20, - }; - - let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); - - assert_eq!(total_fee, 10); // 1% of 1000 - assert_eq!(host_fee, 2); // 20% of 10 - } - - #[test] - fn borrow_fee_calculation_no_host() { - let fees = ReserveFees { - borrow_fee_wad: 10_000_000_000_000_000, // 1% - host_fee_percentage: 0, - }; - - let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); - - assert_eq!(total_fee, 10); // 1% of 1000 - assert_eq!(host_fee, 0); // 0 host fee - } -} +} \ No newline at end of file From b9b345f43d530c00d6299ae215690420b87804f7 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 20:10:06 -0500 Subject: [PATCH 094/191] organized errors --- token-lending/program/src/error.rs | 67 +++++++++++++----------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 04206f9e19e..05eb196b3f1 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -7,7 +7,6 @@ use thiserror::Error; /// Errors that may be returned by the TokenLending program. #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum LendingError { - // @FIXME: reorganize // 0 /// Invalid instruction data passed in. #[error("Failed to unpack instruction data")] @@ -76,83 +75,75 @@ pub enum LendingError { #[error("Token burn failed")] TokenBurnFailed, - // @TODO: these are only used in one place that might be removed + // 20 + // @TODO: this is only used in one place that might be removed. /// Input reserves cannot be the same #[error("Input reserves cannot be the same")] DuplicateReserve, + // @TODO: this is only used in one place that might be removed. /// Input reserves cannot use the same liquidity mint #[error("Input reserves cannot use the same liquidity mint")] DuplicateReserveMint, - - // 20 - // @FIXME: reserve needs a new field to support this - /// This reserve's collateral cannot be used for borrows - #[error("Input reserve has collateral disabled")] - ReserveCollateralDisabled, /// 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, // 25 + /// Withdraw amount too small + #[error("Withdraw amount too small")] + WithdrawTooSmall, + /// Withdraw amount too large + #[error("Withdraw amount too large")] + WithdrawTooLarge, /// 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, - // @FIXME: name + message + // 30 /// Liquidation amount too small #[error("Liquidation amount too small to receive collateral")] LiquidationTooSmall, - // @FIXME: name + message /// Cannot liquidate healthy obligations #[error("Cannot liquidate healthy obligations")] ObligationHealthy, - - // 35 - /// Obligation account limit reached - #[error("Obligation account limit reached")] - ObligationAccountLimit, /// Obligation state stale #[error("Obligation state needs to be refreshed")] ObligationStale, + /// Obligation reserve limit exceeded + #[error("Obligation reserve limit exceeded")] + ObligationReserveLimit, + /// Obligation loan to value limit exceeded + #[error("Obligation loan to value limit exceeded")] + ObligationLoanToValueLimit, - // @FIXME: change name + message - /// Obligation LTV is above the lending market LTV - #[error("Obligation LTV is above the lending market LTV")] - ObligationLTVAboveLendingMarketLTV, - // @FIXME: change name + message - /// Obligation LTV cannot go above the lending market LTV - #[error("Obligation LTV cannot go above the lending market LTV")] - ObligationLTVCannotGoAboveLendingMarketLTV, - + // 35 + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, /// Invalid obligation collateral #[error("Invalid obligation collateral")] InvalidObligationCollateral, - /// Obligation collateral is empty - #[error("Obligation collateral is empty")] - ObligationCollateralEmpty, - /// Obligation collateral withdraw too large - #[error("Obligation collateral withdraw too large for borrowed liquidity")] - ObligationCollateralWithdrawTooLarge, - /// Invalid obligation liquidity #[error("Invalid obligation liquidity")] InvalidObligationLiquidity, + /// Obligation collateral is empty + #[error("Obligation collateral is empty")] + ObligationCollateralEmpty, /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, - /// Obligation liquidity stale - #[error("Obligation liquidity state needs to be refreshed")] - ObligationLiquidityStale, - - /// Negative interest rate - #[error("Interest rate is negative")] - NegativeInterestRate, } impl From for ProgramError { From d5635a27b87a11788d3f4d25571fae3bbfee9d4a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 20:10:55 -0500 Subject: [PATCH 095/191] update docs --- token-lending/program/src/instruction.rs | 49 ++++++++++++------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 225df70ff76..07f53209312 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -88,15 +88,13 @@ pub enum LendingInstruction { }, // 3 - /// Accrue interest and update median quote token price on reserves. + /// Accrue interest and update median quote token price on a reserve. /// /// Accounts expected by this instruction: /// - /// 0. `[]` Clock sysvar - /// 1. `[writable]` Reserve account - /// 2. `[]` Reserve liquidity aggregator account - /// .. `[writable]` Additional reserve account - /// .. `[]` Additional reserve liquidity aggregator account + /// 0. `[writable]` Reserve account + /// 1. `[]` Clock sysvar + /// 2. `[optional]` Reserve liquidity aggregator account RefreshReserve, // 4 @@ -154,7 +152,9 @@ pub enum LendingInstruction { InitObligation, // 7 - /// Refresh an obligation's loan to value ratio. Requires refreshed reserves. + /// Refresh an obligation's accrued interest and collateral and liquidity prices. Requires + /// refreshed reserves, as all obligation collateral deposit reserves in order, followed by all + /// liquidity borrow reserves in order. /// /// Accounts expected by this instruction: /// @@ -162,8 +162,8 @@ pub enum LendingInstruction { /// 1. `[]` Lending market account /// 2. `[]` Clock sysvar /// 3. `[]` Token program id - /// .. `[]` Collateral deposit reserve accounts - refreshed - /// .. `[]` Liquidity borrow reserve accounts - refreshed + /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order + /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order RefreshObligation, // 8 @@ -190,8 +190,7 @@ pub enum LendingInstruction { }, // 9 - /// Withdraw collateral from an obligation. The loan must remain healthy. Requires a - /// recently refreshed obligation. + /// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve. /// /// Accounts expected by this instruction: /// @@ -199,8 +198,8 @@ pub enum LendingInstruction { /// 1. `[writable]` Destination collateral token account /// Minted by withdraw reserve collateral mint. /// $authority can transfer $collateral_amount. - /// 2. `[]` Withdraw reserve account - /// 3. `[writable]` Obligation account + /// 2. `[]` Withdraw reserve account - refreshed + /// 3. `[writable]` Obligation account - refreshed /// 4. `[writable]` Obligation token mint /// 5. `[writable]` Obligation token input account /// 6. `[]` Lending market account @@ -216,18 +215,18 @@ pub enum LendingInstruction { }, // 10 - /// Borrow liquidity from a reserve by depositing collateral tokens. The amount of liquidity is - /// determined by market price. Requires a recently refreshed obligation. + /// Borrow liquidity from a reserve by depositing collateral tokens. Requires a refreshed + /// obligation and reserve. /// /// Accounts expected by this instruction: /// /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account /// 1. `[writable]` Destination liquidity token account /// Minted by borrow reserve liquidity mint. - /// 2. `[writable]` Borrow reserve account + /// 2. `[writable]` Borrow reserve account - refreshed /// 3. `[writable]` Borrow reserve liquidity fee receiver account /// Must be the fee account specified at InitReserve. - /// 4. `[writable]` Obligation account + /// 4. `[writable]` Obligation account - refreshed /// 5. `[]` Lending market account /// 6. `[]` Derived lending market authority /// 7. `[]` Clock sysvar @@ -238,11 +237,11 @@ pub enum LendingInstruction { liquidity_amount: u64, /// Describe how `liquidity_amount` should be treated liquidity_amount_type: AmountType, - // TODO: slippage constraint - https://git.io/JmV67 + // @TODO: slippage constraint - https://git.io/JmV67 }, // 11 - /// Repay borrowed liquidity to a reserve. Requires a recently refreshed obligation. + /// Repay borrowed liquidity to a reserve. Requires a refreshed obligation and reserve. /// /// Accounts expected by this instruction: /// @@ -250,8 +249,8 @@ pub enum LendingInstruction { /// Minted by repay reserve liquidity mint. /// $authority can transfer $liquidity_amount. /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account - /// 2. `[writable]` Repay reserve account - /// 3. `[writable]` Obligation account + /// 2. `[writable]` Repay reserve account - refreshed + /// 3. `[writable]` Obligation account - refreshed /// 4. `[]` Lending market account /// 5. `[]` Derived lending market authority /// 6. `[signer]` User transfer authority ($authority) @@ -266,7 +265,7 @@ pub enum LendingInstruction { // 12 /// Repay borrowed liquidity to a reserve to receive collateral at a discount from an unhealthy - /// obligation. Requires a recently refreshed obligation. + /// obligation. Requires a refreshed obligation and reserves. /// /// Accounts expected by this instruction: /// @@ -275,11 +274,11 @@ pub enum LendingInstruction { /// $authority can transfer $liquidity_amount. /// 1. `[writable]` Destination collateral token account /// Minted by withdraw reserve collateral mint - /// 2. `[writable]` Repay reserve account + /// 2. `[writable]` Repay reserve account - refreshed /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[writable]` Withdraw reserve account + /// 4. `[writable]` Withdraw reserve account - refreshed /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation account + /// 6. `[writable]` Obligation account - refreshed /// 7. `[]` Lending market account /// 8. `[]` Derived lending market authority /// 9. `[signer]` User transfer authority ($authority) From 973a87a1363b830c211c348fac2dc6a85d33d88a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 21:11:56 -0500 Subject: [PATCH 096/191] mass todo completion --- token-lending/program/src/instruction.rs | 49 +-- token-lending/program/src/processor.rs | 302 ++++++++---------- .../program/src/state/last_update.rs | 13 +- .../program/src/state/lending_market.rs | 88 +++-- token-lending/program/src/state/obligation.rs | 293 ++++++++++------- token-lending/program/src/state/reserve.rs | 182 +++++------ .../tests/withdraw_obligation_collateral.rs | 4 +- 7 files changed, 494 insertions(+), 437 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 07f53209312..2d92586a6e8 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -12,7 +12,10 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, sysvar, }; -use std::{convert::TryInto, mem::size_of}; +use std::{ + convert::{TryFrom, TryInto}, + mem::size_of, +}; /// Describe how an input amount should be treated #[derive(Clone, Copy, Debug, PartialEq, FromPrimitive, ToPrimitive)] @@ -302,7 +305,7 @@ impl LendingInstruction { 0 => { let (owner, _rest) = Self::unpack_pubkey(rest)?; let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; - let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; + let (liquidation_threshold, _rest) = Self::unpack_u8(rest)?; Self::InitLendingMarket { owner, loan_to_value_ratio, @@ -319,6 +322,9 @@ impl LendingInstruction { let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; + let (collateral_enabled, rest) = Self::unpack_u8(rest)?; + // @FIXME: convert error to ProgramError + let collateral_enabled = bool::try_from(collateral_enabled)?; let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?; let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?; @@ -330,6 +336,7 @@ impl LendingInstruction { min_borrow_rate, optimal_borrow_rate, max_borrow_rate, + collateral_enabled, fees: ReserveFees { borrow_fee_wad, host_fee_percentage, @@ -353,7 +360,7 @@ impl LendingInstruction { Self::DepositObligationCollateral { collateral_amount } } 9 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + let (collateral_amount, rest) = Self::unpack_u64(rest)?; let (collateral_amount_type, _rest) = Self::unpack_u8(rest)?; let collateral_amount_type = AmountType::from_u8(collateral_amount_type) .ok_or(LendingError::InstructionUnpackError)?; @@ -373,7 +380,7 @@ impl LendingInstruction { } } 11 => { - let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) .ok_or(LendingError::InstructionUnpackError)?; @@ -383,7 +390,7 @@ impl LendingInstruction { } } 12 => { - let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) .ok_or(LendingError::InstructionUnpackError)?; @@ -461,6 +468,7 @@ impl LendingInstruction { min_borrow_rate, optimal_borrow_rate, max_borrow_rate, + collateral_enabled, fees: ReserveFees { borrow_fee_wad, @@ -475,6 +483,7 @@ impl LendingInstruction { buf.extend_from_slice(&min_borrow_rate.to_le_bytes()); buf.extend_from_slice(&optimal_borrow_rate.to_le_bytes()); buf.extend_from_slice(&max_borrow_rate.to_le_bytes()); + buf.extend_from_slice(&u8::from(collateral_enabled).to_le_bytes()); buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } @@ -706,16 +715,19 @@ pub fn redeem_reserve_collateral( } } -// @FIXME: needs optional reserve liquidity aggregator pubkeys /// Creates a `RefreshReserve` instruction -pub fn refresh_reserve(program_id: Pubkey, reserve_pubkeys: Vec) -> Instruction { - let mut accounts = Vec::with_capacity(1 + reserve_pubkeys.len()); - accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)); - accounts.extend( - reserve_pubkeys - .into_iter() - .map(|reserve_pubkey| AccountMeta::new(reserve_pubkey, false)), - ); +pub fn refresh_reserve( + program_id: Pubkey, + reserve_pubkey: Pubkey, + reserve_liquidity_aggregator_pubkey: Option, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + accounts.push(AccountMeta::new(reserve_liquidity_aggregator_pubkey, false)); + } Instruction { program_id, accounts, @@ -751,13 +763,12 @@ pub fn refresh_obligation( lending_market_pubkey: Pubkey, reserve_pubkeys: Vec, ) -> Instruction { - let mut accounts = Vec::with_capacity(4 + reserve_pubkeys.len()); - accounts.extend(vec![ - AccountMeta::new(obligation_pubkey, false) + let mut accounts = vec![ + AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), - ]); + ]; accounts.extend( reserve_pubkeys .into_iter() @@ -766,7 +777,7 @@ pub fn refresh_obligation( Instruction { program_id, accounts, - data: LendingInstruction::RefreshObligationLiquidity.pack(), + data: LendingInstruction::RefreshObligation.pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b05be5370d6..bbc8e9da81a 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -3,20 +3,18 @@ use crate::state::NewReserveLiquidityParams; use crate::{ error::LendingError, - instruction::{init_lending_market, AmountType, LendingInstruction}, - math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, + instruction::{AmountType, LendingInstruction}, + math::{Decimal, Rate, TryDiv, TryMul, WAD}, state::{ BorrowLiquidityResult, LendingMarket, LiquidateObligationResult, NewObligationParams, - NewReserveParams, Obligation, ObligationCollateral, ObligationLiquidity, - RepayLiquidityResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, - WithdrawCollateralResult, MAX_OBLIGATION_DATA, PROGRAM_VERSION, + NewReserveParams, Obligation, RepayLiquidityResult, Reserve, ReserveCollateral, + ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, }, }; -use flux_aggregator; +use flux_aggregator::read_median; use num_traits::FromPrimitive; use solana_program::{ account_info::{next_account_info, AccountInfo}, - clock::Slot, decode_error::DecodeError, entrypoint::ProgramResult, msg, @@ -27,8 +25,11 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; +// @FIXME: 29 | use solana_sdk::account_info::next_account_infos; +// | ^^^^^^^^^^ use of undeclared crate or module `solana_sdk` use solana_sdk::account_info::next_account_infos; -use spl_token::state::Account; +use spl_token::state::{Account, Mint}; +use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( @@ -244,7 +245,7 @@ fn process_init_reserve( return Err(LendingError::InvalidConfig.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; @@ -264,13 +265,11 @@ fn process_init_reserve( assert_rent_exempt(rent, reserve_info)?; assert_uninitialized::(reserve_info)?; - - // @TODO: check if valid - assert_uninitialized(reserve_liquidity_supply_info)?; - assert_uninitialized(reserve_collateral_mint_info)?; - assert_uninitialized(reserve_liquidity_fee_receiver_info)?; - assert_uninitialized(reserve_collateral_supply_info)?; - assert_uninitialized(destination_collateral_info)?; + assert_uninitialized::(reserve_liquidity_supply_info)?; + assert_uninitialized::(reserve_liquidity_fee_receiver_info)?; + assert_uninitialized::(reserve_collateral_mint_info)?; + assert_uninitialized::(reserve_collateral_supply_info)?; + assert_uninitialized::(destination_collateral_info)?; if reserve_liquidity_supply_info.key == source_liquidity_info.key { msg!("Invalid source liquidity account"); @@ -292,17 +291,22 @@ fn process_init_reserve( } let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = - if reserve_liquidity_mint_info.key != &lending_market.quote_token_mint { + if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { + if let Some(_) = account_info_iter.peek() { + msg!("Invalid reserve liquidity aggregator account"); + return Err(LendingError::InvalidAccountInput.into()); + } + // 1 because quote token price is equal to itself + (COption::None, 1) + } else { let aggregator_info = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, aggregator_info)?; - // @TODO: is there a way to verify that aggregator_info uses the base and quote currency? - let answer = flux_aggregator::read_median(aggregator_info)?; - - (COption::Some(*aggregator_info.key), answer.median) - } else { - (COption::None, 1) + // @TODO: is there a way to check that aggregator_info represents the base:quote pair? + ( + COption::Some(*aggregator_info.key), + read_median(aggregator_info)?.median, + ) }; let authority_signer_seeds = &[ @@ -350,14 +354,6 @@ fn process_init_reserve( token_program: token_program_id.clone(), })?; - spl_token_init_mint(TokenInitializeMintParams { - mint: reserve_collateral_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: reserve_liquidity_mint.decimals, - token_program: token_program_id.clone(), - })?; - spl_token_init_account(TokenInitializeAccountParams { account: reserve_liquidity_fee_receiver_info.clone(), mint: reserve_liquidity_mint_info.clone(), @@ -366,6 +362,14 @@ fn process_init_reserve( token_program: token_program_id.clone(), })?; + spl_token_init_mint(TokenInitializeMintParams { + mint: reserve_collateral_mint_info.clone(), + authority: lending_market_authority_info.key, + rent: rent_info.clone(), + decimals: reserve_liquidity_mint.decimals, + token_program: token_program_id.clone(), + })?; + spl_token_init_account(TokenInitializeAccountParams { account: reserve_collateral_supply_info.clone(), mint: reserve_collateral_mint_info.clone(), @@ -405,29 +409,30 @@ fn process_init_reserve( fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); + let reserve_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - while let Some(_) = account_info_iter.peek() { - let reserve_info = next_account_info(account_info_iter)?; - // @FIXME: handle optional aggregator - let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + return Err(LendingError::InvalidAccountOwner.into()); + } - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &reserve.liquidity.aggregator != reserve_liquidity_aggregator_info.key { + if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { + let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; + if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { msg!("Invalid reserve liquidity aggregator account"); return Err(LendingError::InvalidAccountInput.into()); } - let answer = flux_aggregator::read_median(reserve_liquidity_aggregator_info)?; - - reserve.liquidity.median_price = answer.median; - reserve.accrue_interest(clock.slot)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + reserve.liquidity.median_price = read_median(reserve_liquidity_aggregator_info)?.median; + } else if let Some(_) = account_info_iter.peek() { + msg!("Invalid reserve liquidity aggregator account"); + return Err(LendingError::InvalidAccountInput.into()); } + reserve.accrue_interest(clock.slot)?; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + Ok(()) } @@ -484,7 +489,7 @@ fn process_deposit_reserve_liquidity( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - if reserve.last_update.is_stale(clock.slot) { + if reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -575,7 +580,7 @@ fn process_redeem_reserve_collateral( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - if reserve.last_update.is_stale(clock.slot) { + if reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -666,13 +671,12 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: hashmap iteration order not guaranteed, iterate accounts instead - for (deposit_reserve_pubkey, collateral) in obligation.collateral.iter_mut() { + for collateral in obligation.collateral.iter_mut() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if deposit_reserve_pubkey != deposit_reserve_info.key { + if collateral.deposit_reserve != *deposit_reserve_info.key { msg!("Invalid deposit reserve"); return Err(LendingError::InvalidAccountInput.into()); } @@ -692,13 +696,12 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .try_mul(deposit_reserve.liquidity.median_price)?; } - // @FIXME: hashmap iteration order not guaranteed, iterate accounts instead - for (borrow_reserve_pubkey, liquidity) in obligation.liquidity.iter_mut() { + for liquidity in obligation.liquidity.iter_mut() { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if borrow_reserve_pubkey != borrow_reserve_info.key { + if liquidity.borrow_reserve != *borrow_reserve_info.key { msg!("Invalid borrow reserve"); return Err(LendingError::InvalidAccountInput.into()); } @@ -776,12 +779,12 @@ fn process_deposit_obligation_collateral( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - // @TODO: is this necessary? we can set collateral.market_value if we have it though - if deposit_reserve.last_update.is_stale(clock.slot) { + // @TODO: is this necessary? we don't care about market price or interest here yet. + // however, we will if we add the ability to deposit and borrow in one transaction. + if deposit_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } - // @FIXME: moved to lending market; does per-reserve collateral enable still make sense? - if deposit_reserve.config.loan_to_value_ratio == 0 { + if !deposit_reserve.config.collateral_enabled { return Err(LendingError::ReserveCollateralDisabled.into()); } @@ -817,15 +820,12 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let collateral = obligation - .collateral - .entry(*deposit_reserve_info.key) - .or_insert(ObligationCollateral::new()); - if obligation.collateral.len() + obligation.liquidity.len() > MAX_OBLIGATION_DATA { - return Err(LendingError::ObligationAccountLimit.into()); - } + let collateral = obligation.find_or_add_collateral(*deposit_reserve_info.key)?; collateral.deposit(collateral_amount)?; + // @TODO: instead of always marking stale, we could look for an optional + // reserve_liquidity_aggregator_info and update the market value. + // then the depositor could borrow in the same transaction. obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -904,7 +904,7 @@ fn process_withdraw_obligation_collateral( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - if withdraw_reserve.last_update.is_stale(clock.slot) { + if withdraw_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -922,14 +922,8 @@ fn process_withdraw_obligation_collateral( if obligation.last_update < withdraw_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation.collateral.len() == 0 { - return Err(LendingError::ObligationCollateralEmpty.into()); - } - let mut collateral = obligation - .collateral - .get(withdraw_reserve_info.key) - .ok_or(LendingError::InvalidObligationCollateral)?; + let collateral = &mut obligation.find_collateral(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -957,28 +951,50 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: consider moving to Obligation::withdraw_collateral let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; - if loan_to_value > loan_to_value_ratio { - return Err(LendingError::ObligationLTVAboveLendingMarketLTV.into()); - } - if loan_to_value == loan_to_value_ratio { - return Err(LendingError::ObligationLTVCannotGoAboveLendingMarketLTV.into()); + if loan_to_value >= loan_to_value_ratio { + return Err(LendingError::ObligationLoanToValueLimit.into()); } - let WithdrawCollateralResult { - withdraw_amount, - obligation_token_amount, - } = obligation.withdraw_collateral( - collateral_amount, - collateral_amount_type, - &collateral, - loan_to_value_ratio, - obligation_token_mint.supply, - )?; + let max_withdraw_value = obligation.max_withdraw_value(loan_to_value_ratio)?; + + let withdraw_amount = match collateral_amount_type { + AmountType::ExactAmount => { + let withdraw_amount = collateral_amount.min(collateral.deposited_amount); + let withdraw_pct = + Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + let withdraw_value = obligation.collateral_value()?.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + return Err(LendingError::WithdrawTooLarge.into()); + } + + withdraw_amount + } + AmountType::PercentAmount => { + // @FIXME: convert error to ProgramError + let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); + let withdraw_value = max_withdraw_value + .try_mul(withdraw_pct)? + .min(collateral.market_value); + let withdraw_amount = withdraw_value + .try_div(collateral.market_value)? + .try_mul(collateral.deposited_amount)? + .try_floor_u64()?; + + withdraw_amount + } + }; + + let obligation_token_amount = collateral + .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; - // @FIXME: should withdraw_reserve.collateral.mint_total_supply change? + if withdraw_amount == 0 || obligation_token_amount == 0 { + return Err(LendingError::WithdrawTooSmall.into()); + } + + // @TODO: should withdraw_reserve.collateral.mint_total_supply change? + // if so, withdraw reserve needs to be writable. collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1065,13 +1081,7 @@ fn process_borrow_obligation_liquidity( msg!("Invalid borrow reserve liquidity fee receiver account"); return Err(LendingError::InvalidAccountInput.into()); } - // @FIXME: not an issue if reserve currency is quote - if borrow_reserve.liquidity.aggregator.is_none() { - // @FIXME: error message - msg!("Borrow reserve must have a dex market"); - return Err(LendingError::InvalidAccountInput.into()); - } - if borrow_reserve.last_update.is_stale(clock.slot) { + if borrow_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -1089,17 +1099,8 @@ fn process_borrow_obligation_liquidity( if obligation.last_update < borrow_reserve.last_update { return Err(LendingError::ObligationStale.into()); } - if obligation.collateral.len() == 0 { - return Err(LendingError::ObligationCollateralEmpty.into()); - } - let liquidity = obligation - .liquidity - .entry(*borrow_reserve_info.key) - .or_insert(ObligationLiquidity::new()); - if obligation.collateral.len() + obligation.liquidity.len() > MAX_OBLIGATION_DATA { - return Err(LendingError::ObligationAccountLimit.into()); - } + let liquidity = obligation.find_or_add_liquidity(*borrow_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1111,16 +1112,14 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: consider moving let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; - if loan_to_value > loan_to_value_ratio { - return Err(LendingError::ObligationLTVAboveLendingMarketLTV.into()); - } - if loan_to_value == loan_to_value_ratio { - return Err(LendingError::ObligationLTVCannotGoAboveLendingMarketLTV.into()); + if loan_to_value >= loan_to_value_ratio { + return Err(LendingError::ObligationLoanToValueLimit.into()); } + let max_borrow_value = obligation.max_borrow_value(loan_to_value_ratio)?; + let BorrowLiquidityResult { borrow_amount, receive_amount, @@ -1129,12 +1128,13 @@ fn process_borrow_obligation_liquidity( } = borrow_reserve.borrow_liquidity( liquidity_amount, liquidity_amount_type, - &obligation, - loan_to_value_ratio, - token_converter, - &lending_market.quote_token_mint, + max_borrow_value, )?; + if receive_amount == 0 { + return Err(LendingError::BorrowTooSmall.into()); + } + borrow_reserve.liquidity.borrow(borrow_amount)?; liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); @@ -1227,7 +1227,6 @@ fn process_repay_obligation_liquidity( msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } - // @TODO: how is the currency/mint of the liquidity known? if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); @@ -1236,7 +1235,7 @@ fn process_repay_obligation_liquidity( msg!("Invalid destination liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } - if repay_reserve.last_update.is_stale(clock.slot) { + if repay_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -1255,13 +1254,7 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let mut liquidity = obligation - .liquidity - .get(repay_reserve_info.key) - .ok_or(LendingError::InvalidObligationLiquidity)?; - if liquidity.borrowed_amount_wads == Decimal::zero() { - return Err(LendingError::ObligationLiquidityEmpty.into()); - } + let liquidity = &mut obligation.find_liquidity(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1276,12 +1269,19 @@ fn process_repay_obligation_liquidity( let RepayLiquidityResult { settle_amount, repay_amount, - } = repay_reserve.repay_liquidity(liquidity_amount, liquidity_amount_type, &liquidity); + } = repay_reserve.repay_liquidity( + liquidity_amount, + liquidity_amount_type, + liquidity.borrowed_amount_wads, + )?; + + if repay_amount == 0 { + return Err(LendingError::RepayTooSmall.into()); + } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; liquidity.repay(settle_amount); obligation.last_update.mark_stale(); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; @@ -1328,10 +1328,6 @@ fn process_liquidate_obligation( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -1356,7 +1352,7 @@ fn process_liquidate_obligation( msg!("Invalid source liquidity account"); return Err(LendingError::InvalidAccountInput.into()); } - if repay_reserve.last_update.is_stale(clock.slot) { + if repay_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -1376,12 +1372,13 @@ fn process_liquidate_obligation( msg!("Invalid destination collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - if withdraw_reserve.last_update.is_stale(clock.slot) { + if withdraw_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } - // @FIXME: what if a user borrows using the same reserve/mint for collateral? - // maybe liquidation should be allowed, but reduce the bonus + // @TODO: what if a user borrows using the same reserve & mint for collateral? + // this is permitted in borrow_obligation_liquidity and could be used for leverage. + // maybe liquidation should be allowed, but reduce/eliminate the bonus. if repay_reserve_info.key == withdraw_reserve_info.key { return Err(LendingError::DuplicateReserve.into()); } @@ -1407,21 +1404,8 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let mut liquidity = obligation - .liquidity - .get_mut(repay_reserve.key) - .ok_or(LendingError::InvalidObligationLiquidity)?; - if liquidity.borrowed_amount_wads == Decimal::zero() { - return Err(LendingError::ObligationLiquidityEmpty.into()); - } - - let mut collateral = obligation - .collateral - .get_mut(withdraw_reserve_info.key) - .ok_or(LendingError::InvalidObligationCollateral)?; - if collateral.deposited_amount == 0 { - return Err(LendingError::ObligationCollateralEmpty.into()); - } + let liquidity = &mut obligation.find_liquidity(*repay_reserve_info.key)?; + let collateral = &mut obligation.find_collateral(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1433,10 +1417,9 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - // @TODO: consider moving let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); - let obligation_ltv = obligation.loan_to_value()?; - if obligation_ltv < liquidation_threshold { + let loan_to_value = obligation.loan_to_value()?; + if loan_to_value < liquidation_threshold { return Err(LendingError::ObligationHealthy.into()); } @@ -1452,23 +1435,18 @@ fn process_liquidate_obligation( &collateral, )?; - // @TODO: check if this can actually happen now - if withdraw_amount == 0 { + if repay_amount == 0 || withdraw_amount == 0 { return Err(LendingError::LiquidationTooSmall.into()); } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; liquidity.repay(settle_amount); - // @FIXME: should withdraw_reserve.collateral.mint_total_supply change? + // @TODO: should withdraw_reserve.collateral.mint_total_supply change? + // if so, withdraw reserve needs to be writable. collateral.withdraw(withdraw_amount); obligation.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - ObligationCollateral::pack( - collateral, - &mut obligation_collateral_info.data.borrow_mut(), - )?; - ObligationLiquidity::pack(liquidity, &mut obligation_liquidity_info.data.borrow_mut())?; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index 5e4cbcdd04c..ec4dd1b1da2 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -1,14 +1,5 @@ -use crate::{ - error::LendingError, - math::{Decimal, WAD}, -}; -use arrayref::{array_refs, mut_array_refs}; -use solana_program::{ - clock::{Slot, DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, -}; +use crate::error::LendingError; +use solana_program::{clock::Slot, program_error::ProgramError}; use std::cmp::Ordering; /// Number of slots to consider stale after diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index d12bd6a2b1c..d303a97188e 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -6,7 +6,6 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; -// @FIXME: reorder /// Lending market state #[derive(Clone, Debug, Default, PartialEq)] pub struct LendingMarket { @@ -14,18 +13,16 @@ pub struct LendingMarket { pub version: u8, /// Bump seed for derived authority address pub bump_seed: u8, - /// Owner authority which can add new reserves - pub owner: Pubkey, - /// Quote currency token mint - pub quote_token_mint: Pubkey, /// Token program id pub token_program_id: Pubkey, - // @TODO: update doc comment - /// The ratio of the loan to the value of the collateral as a percent + /// Quote currency token mint + pub quote_token_mint: Pubkey, + /// The target ratio of an obligation's borrowed liquidity to deposited collateral as a percent pub loan_to_value_ratio: u8, - // @TODO: update doc comment /// The percent at which an obligation is considered unhealthy pub liquidation_threshold: u8, + /// Owner authority which can add new reserves + pub owner: Pubkey, } impl Sealed for LendingMarket {} @@ -35,10 +32,43 @@ impl IsInitialized for LendingMarket { } } -const LENDING_MARKET_LEN: usize = 160; // 1 + 1 + 32 + 32 + 32 + 1 + 1 + 60 +// @TODO: adjust padding. what's a reasonable number? +const LENDING_MARKET_LEN: usize = 160; // 1 + 1 + 32 + 32 + 1 + 1 + 32 + 60 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, + token_program_id, + quote_token_mint, + loan_to_value_ratio, + liquidation_threshold, + owner, + _padding, + ) = mut_array_refs![ + output, + 1, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + 1, + PUBKEY_BYTES, + 60 + ]; + *version = self.version.to_le_bytes(); + *bump_seed = self.bump_seed.to_le_bytes(); + token_program_id.copy_from_slice(self.token_program_id.as_ref()); + quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); + *loan_to_value_ratio = self.loan_to_value_ratio.to_le_bytes(); + *liquidation_threshold = self.liquidation_threshold.to_le_bytes(); + owner.copy_from_slice(self.owner.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]; @@ -46,13 +76,23 @@ impl Pack for LendingMarket { let ( version, bump_seed, - owner, quote_token_mint, token_program_id, loan_to_value_ratio, liquidation_threshold, + owner, _padding, - ) = array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 1, 1, 60]; + ) = array_refs![ + input, + 1, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + 1, + PUBKEY_BYTES, + 60 + ]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -61,33 +101,11 @@ impl Pack for LendingMarket { Ok(Self { version, bump_seed: u8::from_le_bytes(*bump_seed), - owner: Pubkey::new_from_array(*owner), - quote_token_mint: Pubkey::new_from_array(*quote_token_mint), token_program_id: Pubkey::new_from_array(*token_program_id), + quote_token_mint: Pubkey::new_from_array(*quote_token_mint), loan_to_value_ratio: u8::from_le_bytes(*loan_to_value_ratio), liquidation_threshold: u8::from_le_bytes(*liquidation_threshold), + owner: Pubkey::new_from_array(*owner), }) } - - 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_token_mint, - token_program_id, - loan_to_value_ratio, - liquidation_threshold, - _padding, - ) = mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 1, 1, 60]; - *version = self.version.to_le_bytes(); - *bump_seed = self.bump_seed.to_le_bytes(); - owner.copy_from_slice(self.owner.as_ref()); - quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); - token_program_id.copy_from_slice(self.token_program_id.as_ref()); - *loan_to_value_ratio = self.loan_to_value_ratio.to_le_bytes(); - *liquidation_threshold = self.liquidation_threshold.to_le_bytes(); - } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 695425478cd..bb88b9941b8 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -1,7 +1,6 @@ use super::*; use crate::{ error::LendingError, - instruction::AmountType, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; @@ -12,16 +11,14 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; -use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; -// @TODO: rename / relocate; true max is potentially 28 -/// Max number of collateral and liquidity accounts combined for an obligation -pub const MAX_OBLIGATION_DATA: usize = 10; +/// Max number of collateral and liquidity reserve accounts combined for an obligation +pub const MAX_OBLIGATION_RESERVES: usize = 10; /// Borrow obligation state #[derive(Clone, Debug, Default, PartialEq)] -pub struct Obligation<'a> { +pub struct Obligation { /// Version of the struct pub version: u8, /// Last update to collateral, liquidity, or their market values @@ -29,9 +26,9 @@ pub struct Obligation<'a> { /// Lending market address pub lending_market: Pubkey, /// Collateral state for the obligation, keyed by deposit reserve address - pub collateral: HashMap, + pub collateral: Vec, /// Liquidity state for the obligation, keyed by borrow reserve address - pub liquidity: HashMap, + pub liquidity: Vec, } /// Create new obligation @@ -54,81 +51,148 @@ impl Obligation { version: PROGRAM_VERSION, last_update: LastUpdate::new(current_slot), lending_market, - collateral: HashMap::new(), - liquidity: HashMap::new(), + collateral: vec![], + liquidity: vec![], } } - /// Calculate the ratio of liquidity market value to collateral market value - pub fn loan_to_value(&self) -> Result { + // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, + // but that seems sloppy. + /// Calculate the collateral market value + pub fn collateral_value(&self) -> Result { + let mut collateral_value = Decimal::zero(); + for collateral in self.collateral { + collateral_value = collateral_value.try_add(collateral.market_value)?; + } + Ok(collateral_value) + } + + // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, + // but that seems sloppy. + /// Calculate the liquidity market value + pub fn liquidity_value(&self) -> Result { let mut liquidity_value = Decimal::zero(); - for (_, liquidity) in self.liquidity { + for liquidity in self.liquidity { liquidity_value = liquidity_value.try_add(liquidity.market_value)?; } + Ok(liquidity_value) + } - let mut collateral_value = Decimal::zero(); - for (_, collateral) in self.collateral { - collateral_value = collateral_value.try_add(collateral.market_value)?; + /// Calculate the ratio of liquidity market value to collateral market value + pub fn loan_to_value(&self) -> Result { + let collateral_value = self.collateral_value()?; + if collateral_value == Decimal::zero() { + return Err(LendingError::ObligationCollateralEmpty.into()); } + // @FIXME: convert Decimal to Rate + self.liquidity_value()?.try_div(collateral_value) + } + + /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio + pub fn max_withdraw_value(&self, loan_to_value_ratio: Rate) -> Result { + let min_collateral_value = self.liquidity_value()?.try_div(loan_to_value_ratio)?; + self.collateral_value()?.try_sub(min_collateral_value) + } - // @TODO: error if collateral value is zero? - liquidity_value.try_div(collateral_value) + /// Calculate the maximum liquidity value that can be borrowed for a given loan to value ratio + pub fn max_borrow_value(&self, loan_to_value_ratio: Rate) -> Result { + self.collateral_value()? + .try_mul(loan_to_value_ratio)? + .try_sub(self.liquidity_value()?) } - pub fn withdraw_collateral( + /// Calculate the maximum liquidation amount for a given liquidity + pub fn max_liquidation_amount( &self, - collateral_amount: u64, - collateral_amount_type: AmountType, - obligation_collateral: &ObligationCollateral, - loan_to_value_ratio: Rate, - obligation_token_supply: u64, - ) -> Result { - let min_collateral_value = self.liquidity_value.try_div(loan_to_value_ratio)?; - let max_withdraw_value = self.collateral_value.try_sub(min_collateral_value)?; - - let withdraw_amount = match collateral_amount_type { - AmountType::ExactAmount => { - let withdraw_amount = collateral_amount.min(obligation_collateral.deposited_amount); - let withdraw_pct = Decimal::from(withdraw_amount) - .try_div(obligation_collateral.deposited_amount)?; - let withdraw_value = self.collateral_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - return Err(LendingError::ObligationCollateralWithdrawTooLarge.into()); - } - - withdraw_amount - } - AmountType::PercentAmount => { - let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); - let withdraw_value = max_withdraw_value - .try_mul(withdraw_pct)? - .min(obligation_collateral.value); - let withdraw_amount = withdraw_value - .try_div(obligation_collateral.value)? - .try_mul(obligation_collateral.deposited_amount)? - .try_floor_u64()?; - - withdraw_amount - } - }; + liquidity: &ObligationLiquidity, + ) -> Result { + let max_liquidation_value = self + .liquidity_value()? + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? + .min(liquidity.market_value); + let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; + liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct) + } - let obligation_token_amount = obligation_collateral - .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_supply)?; + /// Find collateral by deposit reserve + pub fn find_collateral( + &self, + deposit_reserve: Pubkey, + ) -> Result<&ObligationCollateral, ProgramError> { + if self.collateral.len() == 0 { + return Err(LendingError::ObligationCollateralEmpty.into()); + } + if let Some(collateral) = self + .collateral + .iter() + .find(|&collateral| collateral.deposit_reserve == deposit_reserve) + { + Ok(collateral) + } else { + Err(LendingError::InvalidObligationCollateral.into()) + } + } - Ok(WithdrawCollateralResult { - withdraw_amount, - obligation_token_amount, - }) + /// Find or add collateral by deposit reserve + pub fn find_or_add_collateral( + &mut self, + deposit_reserve: Pubkey, + ) -> Result<&mut ObligationCollateral, ProgramError> { + if let Some(collateral) = self + .collateral + .iter_mut() + .find(|&collateral| collateral.deposit_reserve == deposit_reserve) + { + Ok(collateral) + } else { + self.collateral + .push(ObligationCollateral::new(deposit_reserve)); + if self.collateral.len() + self.liquidity.len() > MAX_OBLIGATION_RESERVES { + return Err(LendingError::ObligationReserveLimit.into()); + } + Ok(self.collateral.last_mut().unwrap()) + } + } + + /// Find liquidity by borrow reserve + pub fn find_liquidity( + &self, + borrow_reserve: Pubkey, + ) -> Result<&ObligationLiquidity, ProgramError> { + if self.liquidity.len() == 0 { + return Err(LendingError::ObligationLiquidityEmpty.into()); + } + if let Some(liquidity) = self + .liquidity + .iter() + .find(|&liquidity| liquidity.borrow_reserve == borrow_reserve) + { + Ok(liquidity) + } else { + Err(LendingError::InvalidObligationLiquidity.into()) + } } -} -/// Withdraw collateral result -#[derive(Debug)] -pub struct WithdrawCollateralResult { - /// Collateral tokens to withdraw - withdraw_amount: u64, - /// Obligation tokens to burn - obligation_token_amount: u64, + /// Find or add liquidity by borrow reserve + pub fn find_or_add_liquidity( + &mut self, + borrow_reserve: Pubkey, + ) -> Result<&mut ObligationLiquidity, ProgramError> { + if let Some(liquidity) = self + .liquidity + .iter_mut() + .find(|&liquidity| liquidity.borrow_reserve == borrow_reserve) + { + Ok(liquidity) + } else { + self.liquidity + .push(ObligationLiquidity::new(borrow_reserve)); + if self.collateral.len() + self.liquidity.len() > MAX_OBLIGATION_RESERVES { + return Err(LendingError::ObligationReserveLimit.into()); + } + Ok(self.liquidity.last_mut().unwrap()) + } + } } impl Sealed for Obligation {} @@ -141,6 +205,8 @@ impl IsInitialized for Obligation { /// Obligation collateral state #[derive(Clone, Debug, Default, PartialEq)] 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 @@ -149,10 +215,10 @@ pub struct ObligationCollateral { impl ObligationCollateral { /// Create new obligation collateral - pub fn new() -> Self { + pub fn new(deposit_reserve: Pubkey) -> Self { Self { + deposit_reserve, deposited_amount: 0, - // @TODO: should this be initialized with a real value on deposit? market_value: Decimal::zero(), } } @@ -191,6 +257,8 @@ impl ObligationCollateral { /// Obligation liquidity state #[derive(Clone, Debug, Default, PartialEq)] 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 @@ -201,11 +269,11 @@ pub struct ObligationLiquidity { impl ObligationLiquidity { /// Create new obligation liquidity - pub fn new() -> Self { + pub fn new(borrow_reserve: Pubkey) -> Self { Self { + borrow_reserve, cumulative_borrow_rate_wads: Decimal::one(), borrowed_amount_wads: Decimal::zero(), - // @TODO: should this be initialized with a real value on borrow? market_value: Decimal::zero(), } } @@ -217,8 +285,8 @@ impl ObligationLiquidity { } /// Increase borrowed liquidity - pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { - self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; + pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?; Ok(()) } @@ -241,9 +309,10 @@ impl ObligationLiquidity { } } -const OBLIGATION_COLLATERAL_LEN: usize = 40; // 32 + 8 -const OBLIGATION_LIQUIDITY_LEN: usize = 64; // 32 + 16 + 16 -const OBLIGATION_LEN: usize = 716; // 1 + 8 + 1 + 32 + 16 + 16 + 1 + 1 + (64 * 10) +// @TODO: adjust padding. what's a reasonable number? +const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 +const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 +const OBLIGATION_LEN: usize = 820; // 1 + 8 + 1 + 32 + 1 + 1 + (56 * 1) + (80 * 9) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -266,33 +335,41 @@ impl Pack for Obligation { PUBKEY_BYTES, 1, 1, - OBLIGATION_LIQUIDITY_LEN * MAX_OBLIGATION_DATA + OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); lending_market.copy_from_slice(self.lending_market.as_ref()); + // @FIXME: can't return error here *collateral_len = u8::try_from(self.collateral.len())?.to_le_bytes(); + // @FIXME: can't return error here *liquidity_len = u8::try_from(self.liquidity.len())?.to_le_bytes(); let mut offset = 0; - for (deposit_reserve, collateral) in self.collateral.iter() { + for collateral in self.collateral { let collateral_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (reserve, deposited_amount) = mut_array_refs![collateral_flat, PUBKEY_BYTES, 8]; - reserve.copy_from_slice(deposit_reserve.as_ref()); + let (deposit_reserve, deposited_amount, market_value) = + mut_array_refs![collateral_flat, PUBKEY_BYTES, 8, 16]; + 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; } - for (borrow_reserve, liquidity) in self.liquidity.iter() { + for liquidity in self.liquidity { let liquidity_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (reserve, cumulative_borrow_rate_wads, borrowed_amount_wads) = - mut_array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16]; - reserve.copy_from_slice(borrow_reserve.as_ref()); - *cumulative_borrow_rate_wads = liquidity.cumulative_borrow_rate_wads.to_le_bytes(); - *borrowed_amount_wads = liquidity.borrowed_amount_wads.to_le_bytes(); + let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) = + mut_array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16, 16]; + 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; } } @@ -316,37 +393,36 @@ impl Pack for Obligation { PUBKEY_BYTES, 1, 1, - OBLIGATION_LIQUIDITY_LEN * MAX_OBLIGATION_DATA + OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; let collateral_len = u8::from_le_bytes(*collateral_len); let liquidity_len = u8::from_le_bytes(*liquidity_len); - let mut collateral = HashMap::with_capacity(usize::from(collateral_len)); - let mut liquidity = HashMap::with_capacity(usize::from(liquidity_len)); + let mut collateral = Vec::with_capacity(usize::from(collateral_len)); + let mut liquidity = Vec::with_capacity(usize::from(liquidity_len)); let mut offset = 0; - for _ in collateral_len { + for _ in 0..collateral_len { let collateral_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; - let (deposit_reserve, deposited_amount) = array_refs![collateral_flat, PUBKEY_BYTES, 8]; - collateral.insert( - Pubkey::new(deposit_reserve), - ObligationCollateral { - deposited_amount: u64::from_le_bytes(*deposited_amount), - }, - ); + let (deposit_reserve, deposited_amount, market_value) = + array_refs![collateral_flat, PUBKEY_BYTES, 8, 16]; + collateral.push(ObligationCollateral { + deposit_reserve: Pubkey::new(deposit_reserve), + deposited_amount: u64::from_le_bytes(*deposited_amount), + market_value: unpack_decimal(market_value), + }); offset += OBLIGATION_COLLATERAL_LEN; } - for _ in liquidity_len { + for _ in 0..liquidity_len { let liquidity_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; - let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads) = - array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16]; - liquidity.insert( - Pubkey::new(borrow_reserve), - ObligationLiquidity { - cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), - borrowed_amount_wads: unpack_decimal(borrowed_amount_wads), - }, - ); + let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) = + array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16, 16]; + liquidity.push(ObligationLiquidity { + borrow_reserve: Pubkey::new(borrow_reserve), + 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; } @@ -354,7 +430,8 @@ impl Pack for Obligation { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), - stale: bool::from(u8::from_le_bytes(*last_update_stale)), + // @FIXME: convert error to ProgramError + stale: bool::try_from(u8::from_le_bytes(*last_update_stale))?, }, lending_market: Pubkey::new_from_array(*lending_market), collateral, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3a376e661e8..1fc4b54e94d 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -159,28 +159,19 @@ impl Reserve { &self, liquidity_amount: u64, liquidity_amount_type: AmountType, - obligation: &Obligation, - loan_to_value_ratio: Rate, - token_converter: impl TokenConverter, - quote_token_mint: &Pubkey, + max_borrow_value: Decimal, ) -> Result { - let max_borrow_value = obligation - .collateral_value - .try_mul(loan_to_value_ratio)? - .try_sub(obligation.liquidity_value)?; - match liquidity_amount_type { AmountType::ExactAmount => { let receive_amount = liquidity_amount; + let borrow_amount = Decimal::from(receive_amount); let (borrow_fee, host_fee) = self .config .fees - .calculate_borrow_fees(receive_amount, FeeCalculation::Exclusive)?; - let borrow_amount = receive_amount - .checked_add(borrow_fee) - .ok_or(LendingError::MathOverflow)?; - let borrow_value = - token_converter.convert(borrow_amount.into(), &self.liquidity.mint_pubkey)?; + .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; + + let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; + let borrow_value = borrow_amount.try_mul(self.liquidity.median_price)?; if borrow_value > max_borrow_value { return Err(LendingError::BorrowTooLarge.into()); } @@ -193,17 +184,18 @@ impl Reserve { }) } AmountType::PercentAmount => { + // @FIXME: convert error to ProgramError let borrow_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); let borrow_value = max_borrow_value.try_mul(borrow_pct)?; - let borrow_amount = token_converter - .convert(borrow_value, "e_token_mint)? - .try_floor_u64()? - .min(self.liquidity.available_amount); + let borrow_amount = borrow_value + .try_div(self.liquidity.median_price)? + .min(self.liquidity.available_amount.into()); let (origination_fee, host_fee) = self .config .fees .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?; let receive_amount = borrow_amount + .try_floor_u64()? .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; @@ -221,20 +213,17 @@ impl Reserve { &self, liquidity_amount: u64, liquidity_amount_type: AmountType, - obligation_liquidity: &ObligationLiquidity, + borrow_amount: Decimal, ) -> Result { let settle_amount = match liquidity_amount_type { - AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_amount_wads) - } + AmountType::ExactAmount => Decimal::from(liquidity_amount).min(borrow_amount), AmountType::PercentAmount => { + // @FIXME: convert error to ProgramError let settle_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); - obligation_liquidity - .borrowed_amount_wads - .try_mul(settle_pct)? + borrow_amount.try_mul(settle_pct)? } }; - let repay_amount = if settle_amount == obligation_liquidity.borrowed_amount_wads { + let repay_amount = if settle_amount == borrow_amount { settle_amount.try_ceil_u64()? } else { settle_amount.try_floor_u64()? @@ -252,75 +241,57 @@ impl Reserve { liquidity_amount: u64, liquidity_amount_type: AmountType, obligation: &Obligation, - obligation_liquidity: &ObligationLiquidity, - obligation_collateral: &ObligationCollateral, + liquidity: &ObligationLiquidity, + collateral: &ObligationCollateral, ) -> Result { - let max_withdraw_amount = Decimal::from(obligation_collateral.deposited_amount); let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; let liquidate_amount = match liquidity_amount_type { AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(obligation_liquidity.borrowed_amount_wads) + Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) } AmountType::PercentAmount => { + // @FIXME: convert error to ProgramError let liquidate_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); - obligation_liquidity - .borrowed_amount_wads - .try_mul(liquidate_pct)? + liquidity.borrowed_amount_wads.try_mul(liquidate_pct)? } }; - // Close out obligations that are too small to liquidate normally - if obligation_liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() { - let settle_amount = obligation_liquidity.borrowed_amount_wads; - let settle_value = obligation_liquidity - .value - .try_mul(bonus_rate)? - .min(obligation_collateral.value); - let settle_pct = settle_value.try_div(obligation_collateral.value)?; - let repay_amount = liquidate_amount.min(settle_amount).try_ceil_u64()?; - - let collateral_amount = max_withdraw_amount.try_mul(settle_pct)?; - let withdraw_amount = if collateral_amount == max_withdraw_amount { - collateral_amount.try_ceil_u64()? + let (settle_amount, settle_pct, repay_amount) = + if liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() { + // Close out obligations that are too small to liquidate normally + let settle_amount = liquidity.borrowed_amount_wads; + let settle_value = liquidity + .market_value + .try_mul(bonus_rate)? + .min(collateral.market_value); + let settle_pct = settle_value.try_div(collateral.market_value)?; + let repay_amount = liquidate_amount.min(settle_amount).try_ceil_u64()?; + + (settle_amount, settle_pct, repay_amount) } else { - collateral_amount.try_floor_u64()? + let max_liquidation_amount = obligation.max_liquidation_amount(liquidity)?; + let liquidation_amount = liquidate_amount.min(max_liquidation_amount); + let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?; + + let settle_value = liquidity + .market_value + .try_mul(liquidation_pct)? + .try_mul(bonus_rate)? + .min(collateral.market_value); + let settle_pct = settle_value.try_div(collateral.market_value)?; + let settle_amount = liquidation_amount.try_mul(settle_pct)?; + + let repay_amount = if settle_amount == max_liquidation_amount { + settle_amount.try_ceil_u64()? + } else { + settle_amount.try_floor_u64()? + }; + + (settle_amount, settle_pct, repay_amount) }; - return Ok(LiquidateObligationResult { - settle_amount, - repay_amount, - withdraw_amount, - }); - } - - let max_liquidation_value = obligation - .liquidity_value - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .min(obligation_liquidity.value); - let max_liquidation_pct = max_liquidation_value.try_div(obligation_liquidity.value)?; - let max_liquidation_amount = obligation_liquidity - .borrowed_amount_wads - .try_mul(max_liquidation_pct)?; - - let liquidation_amount = liquidate_amount.min(max_liquidation_amount); - let liquidation_pct = - liquidation_amount.try_div(obligation_liquidity.borrowed_amount_wads)?; - - let settle_value = obligation_liquidity - .value - .try_mul(liquidation_pct)? - .try_mul(bonus_rate)? - .min(obligation_collateral.value); - let settle_pct = settle_value.try_div(obligation_collateral.value)?; - let settle_amount = liquidation_amount.try_mul(settle_pct)?; - - let repay_amount = if settle_amount == max_liquidation_amount { - settle_amount.try_ceil_u64()? - } else { - settle_amount.try_floor_u64()? - }; - + let max_withdraw_amount = Decimal::from(collateral.deposited_amount); let collateral_amount = max_withdraw_amount.try_mul(settle_pct)?; let withdraw_amount = if collateral_amount == max_withdraw_amount { collateral_amount.try_ceil_u64()? @@ -354,7 +325,7 @@ pub struct NewReserveParams { #[derive(Debug)] pub struct BorrowLiquidityResult { /// Total amount of borrow including fees - pub borrow_amount: u64, + pub borrow_amount: Decimal, /// Borrow amount portion of total amount pub receive_amount: u64, /// Loan origination fee @@ -437,16 +408,18 @@ impl ReserveLiquidity { } /// Subtract borrow amount from available liquidity and add to borrows - pub fn borrow(&mut self, borrow_amount: u64) -> ProgramResult { - if borrow_amount > self.available_amount { + pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { + // @TODO: should this be rounded differently? + let borrow_rounded = borrow_amount.try_ceil_u64()?; + if borrow_rounded > self.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } self.available_amount = self .available_amount - .checked_sub(borrow_amount) + .checked_sub(borrow_rounded) .ok_or(LendingError::MathOverflow)?; - self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount.into())?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?; Ok(()) } @@ -491,8 +464,6 @@ impl ReserveLiquidity { } } - - /// Reserve liquidity #[derive(Clone, Debug, Default, PartialEq)] pub struct NewReserveLiquidityParams { @@ -600,6 +571,8 @@ pub struct ReserveConfig { pub optimal_borrow_rate: u8, /// Max borrow APY pub max_borrow_rate: u8, + /// Collateral is enabled for borrows + pub collateral_enabled: bool, /// Program owner fees assessed, separate from gains due to interest accrual pub fees: ReserveFees, } @@ -634,12 +607,12 @@ impl ReserveFees { /// Calculate the owner and host fees on borrow pub fn calculate_borrow_fees( &self, - borrow_amount: u64, + borrow_amount: Decimal, fee_calculation: FeeCalculation, ) -> Result<(u64, u64), ProgramError> { let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); - if borrow_fee_rate > Rate::zero() && borrow_amount > 0 { + if borrow_fee_rate > Rate::zero() && borrow_amount > Decimal::zero() { let need_to_assess_host_fee = host_fee_rate > Rate::zero(); let minimum_fee = if need_to_assess_host_fee { 2 // 1 token to owner, 1 to host @@ -648,10 +621,12 @@ impl ReserveFees { }; let borrow_fee_amount = match fee_calculation { - FeeCalculation::Exclusive => borrow_fee_rate.try_mul(borrow_amount)?, - FeeCalculation::Inclusive => borrow_fee_rate - .try_div(borrow_fee_rate.try_add(Rate::one())?)? - .try_mul(borrow_amount)?, + FeeCalculation::Exclusive => borrow_amount.try_mul(borrow_fee_rate)?, + FeeCalculation::Inclusive => { + let borrow_fee_rate = + borrow_fee_rate.try_div(borrow_fee_rate.try_add(Rate::one())?)?; + borrow_amount.try_mul(borrow_fee_rate)? + } }; let borrow_fee = borrow_fee_amount.try_round_u64()?.max(minimum_fee); @@ -662,7 +637,7 @@ impl ReserveFees { 0 }; - if borrow_fee >= borrow_amount { + if Decimal::from(borrow_fee) >= borrow_amount { Err(LendingError::BorrowTooSmall.into()) } else { Ok((borrow_fee, host_fee)) @@ -680,7 +655,8 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 609; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 300 +// @TODO: adjust padding. what's a reasonable number? +const RESERVE_LEN: usize = 610; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 300 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -708,6 +684,7 @@ impl Pack for Reserve { min_borrow_rate, optimal_borrow_rate, max_borrow_rate, + collateral_enabled, borrow_fee_wad, host_fee_percentage, _padding, @@ -734,6 +711,7 @@ impl Pack for Reserve { 1, 1, 1, + 1, 8, 1, 300 @@ -771,6 +749,7 @@ impl Pack for Reserve { *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); + *collateral_enabled = u8::from(self.config.collateral_enabled).to_le_bytes(); *borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); *host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); } @@ -801,6 +780,7 @@ impl Pack for Reserve { min_borrow_rate, optimal_borrow_rate, max_borrow_rate, + collateral_enabled, borrow_fee_wad, host_fee_percentage, __padding, @@ -827,6 +807,7 @@ impl Pack for Reserve { 1, 1, 1, + 1, 8, 1, 300 @@ -835,7 +816,8 @@ impl Pack for Reserve { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), - stale: bool::from(u8::from_le_bytes(*last_update_stale)), + // @FIXME: convert error to ProgramError + stale: bool::try_from(u8::from_le_bytes(*last_update_stale))?, }, lending_market: Pubkey::new_from_array(*lending_market), liquidity: ReserveLiquidity { @@ -860,6 +842,8 @@ impl Pack for Reserve { min_borrow_rate: u8::from_le_bytes(*min_borrow_rate), optimal_borrow_rate: u8::from_le_bytes(*optimal_borrow_rate), max_borrow_rate: u8::from_le_bytes(*max_borrow_rate), + // @FIXME: convert error to ProgramError + collateral_enabled: bool::try_from(u8::from_le_bytes(*collateral_enabled))?, fees: ReserveFees { borrow_fee_wad: u64::from_le_bytes(*borrow_fee_wad), host_fee_percentage: u8::from_le_bytes(*host_fee_percentage), @@ -867,4 +851,4 @@ impl Pack for Reserve { }, }) } -} \ No newline at end of file +} diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 95a2535152e..5d11819af83 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -328,9 +328,7 @@ async fn test_withdraw_below_required() { .unwrap(), TransactionError::InstructionError( 3, - InstructionError::Custom( - LendingError::ObligationLTVCannotGoAboveLendingMarketLTV as u32 - ) + InstructionError::Custom(LendingError::ObligationAtLimit as u32) ) ); } From 5ac820f4ffa045a60851625ef0c2d810c365b68e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 21:56:40 -0500 Subject: [PATCH 097/191] remove bad underscore --- token-lending/program/src/instruction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 2d92586a6e8..df99a1b2932 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -303,7 +303,7 @@ impl LendingInstruction { .ok_or(LendingError::InstructionUnpackError)?; Ok(match tag { 0 => { - let (owner, _rest) = Self::unpack_pubkey(rest)?; + let (owner, rest) = Self::unpack_pubkey(rest)?; let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; let (liquidation_threshold, _rest) = Self::unpack_u8(rest)?; Self::InitLendingMarket { From 64d006377153ee71b3ddc43871935d0fd4355b95 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 22:08:23 -0500 Subject: [PATCH 098/191] no longer using hashmap --- token-lending/program/src/state/obligation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index bb88b9941b8..fb9f08a7c7e 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -25,9 +25,9 @@ pub struct Obligation { pub last_update: LastUpdate, /// Lending market address pub lending_market: Pubkey, - /// Collateral state for the obligation, keyed by deposit reserve address + /// Collateral state for the obligation, unique by deposit reserve address pub collateral: Vec, - /// Liquidity state for the obligation, keyed by borrow reserve address + /// Liquidity state for the obligation, unique by borrow reserve address pub liquidity: Vec, } From c4c8af7e2490c05ae7aa1df83935c03b8a4f302f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 22 Mar 2021 22:17:41 -0500 Subject: [PATCH 099/191] use Rate for percent --- token-lending/program/src/processor.rs | 2 +- token-lending/program/src/state/reserve.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index bbc8e9da81a..32ef1d98a0d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -973,7 +973,7 @@ fn process_withdraw_obligation_collateral( } AmountType::PercentAmount => { // @FIXME: convert error to ProgramError - let withdraw_pct = Decimal::from_percent(u8::try_from(collateral_amount)?); + let withdraw_pct = Rate::from_percent(u8::try_from(collateral_amount)?); let withdraw_value = max_withdraw_value .try_mul(withdraw_pct)? .min(collateral.market_value); diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 1fc4b54e94d..285ee76f48f 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -185,7 +185,7 @@ impl Reserve { } AmountType::PercentAmount => { // @FIXME: convert error to ProgramError - let borrow_pct = Decimal::from_percent(u8::try_from(liquidity_amount)?); + let borrow_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); let borrow_value = max_borrow_value.try_mul(borrow_pct)?; let borrow_amount = borrow_value .try_div(self.liquidity.median_price)? From d92cf6cabaa64bf2bc2d3ea15129d9933641536d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 13:36:51 -0500 Subject: [PATCH 100/191] rm unused error --- token-lending/program/src/error.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 05eb196b3f1..3c2d828515f 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -54,9 +54,9 @@ pub enum LendingError { /// Math operation overflow #[error("Math operation overflow")] MathOverflow, - /// Memory is too small - #[error("Memory is too small")] - MemoryTooSmall, + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, // 15 /// Token initialize mint failed @@ -129,9 +129,6 @@ pub enum LendingError { ObligationLoanToValueLimit, // 35 - /// Negative interest rate - #[error("Interest rate is negative")] - NegativeInterestRate, /// Invalid obligation collateral #[error("Invalid obligation collateral")] InvalidObligationCollateral, From eb3f5d8c0751b8b5ec7fa8362d40746f345d4f45 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 13:45:55 -0500 Subject: [PATCH 101/191] fix @FIXME (type conversions) --- token-lending/program/src/instruction.rs | 27 ++++++++++++++++--- token-lending/program/src/processor.rs | 7 +---- token-lending/program/src/state/mod.rs | 14 ++++++++++ token-lending/program/src/state/obligation.rs | 18 +++++-------- token-lending/program/src/state/reserve.rs | 19 +++++-------- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index df99a1b2932..15cf37da04c 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -13,7 +13,7 @@ use solana_program::{ sysvar, }; use std::{ - convert::{TryFrom, TryInto}, + convert::{TryInto}, mem::size_of, }; @@ -322,9 +322,7 @@ impl LendingInstruction { let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; - let (collateral_enabled, rest) = Self::unpack_u8(rest)?; - // @FIXME: convert error to ProgramError - let collateral_enabled = bool::try_from(collateral_enabled)?; + let (collateral_enabled, rest) = Self::unpack_bool(rest)?; let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?; let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?; @@ -431,6 +429,27 @@ impl LendingInstruction { } } + fn unpack_bool(input: &[u8]) -> Result<(bool, &[u8]), ProgramError> { + if !input.is_empty() { + let (bool, rest) = input.split_at(1); + let bool = bool + .get(..1) + .and_then(|slice| slice.try_into().ok()) + .map(u8::from_le_bytes) + .and_then(|bool| match bool { + // @TODO bool::try_from(u8).ok() fails with + // ^^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `bool` + 0 => Some(false), + 1 => Some(true), + _ => None, + }) + .ok_or(LendingError::InstructionUnpackError)?; + Ok((bool, rest)) + } else { + Err(LendingError::InstructionUnpackError.into()) + } + } + fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { if input.len() >= PUBKEY_BYTES { let (key, rest) = input.split_at(PUBKEY_BYTES); diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 32ef1d98a0d..1cddce8cdf4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -25,11 +25,7 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; -// @FIXME: 29 | use solana_sdk::account_info::next_account_infos; -// | ^^^^^^^^^^ use of undeclared crate or module `solana_sdk` -use solana_sdk::account_info::next_account_infos; use spl_token::state::{Account, Mint}; -use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( @@ -972,8 +968,7 @@ fn process_withdraw_obligation_collateral( withdraw_amount } AmountType::PercentAmount => { - // @FIXME: convert error to ProgramError - let withdraw_pct = Rate::from_percent(u8::try_from(collateral_amount)?); + let withdraw_pct = Rate::from_percent(collateral_amount as u8); let withdraw_value = max_withdraw_value .try_mul(withdraw_pct)? .min(collateral.market_value); diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index e5e74577809..5a50649c362 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -68,6 +68,20 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { Decimal::from_scaled_val(u128::from_le_bytes(*src)) } +fn pack_bool(bool: bool, dst: &mut [u8; 1]) { + *dst = (bool as u8).to_le_bytes() +} + +fn unpack_bool(src: &[u8; 1]) -> Result { + // @TODO bool::try_from(u8).ok() fails with + // ^^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `bool` + match u8::from_le_bytes(*src) { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ProgramError::InvalidAccountData), + } +} + #[cfg(test)] mod test { use super::*; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index fb9f08a7c7e..ce8000fd8ba 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -84,8 +84,7 @@ impl Obligation { if collateral_value == Decimal::zero() { return Err(LendingError::ObligationCollateralEmpty.into()); } - // @FIXME: convert Decimal to Rate - self.liquidity_value()?.try_div(collateral_value) + Rate::try_from(self.liquidity_value()?.try_div(collateral_value)?) } /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio @@ -340,12 +339,10 @@ impl Pack for Obligation { *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); - *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); + pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); - // @FIXME: can't return error here - *collateral_len = u8::try_from(self.collateral.len())?.to_le_bytes(); - // @FIXME: can't return error here - *liquidity_len = u8::try_from(self.liquidity.len())?.to_le_bytes(); + *collateral_len = u8::try_from(self.collateral.len()).unwrap().to_le_bytes(); + *liquidity_len = u8::try_from(self.liquidity.len()).unwrap().to_le_bytes(); let mut offset = 0; for collateral in self.collateral { @@ -398,8 +395,8 @@ impl Pack for Obligation { let collateral_len = u8::from_le_bytes(*collateral_len); let liquidity_len = u8::from_le_bytes(*liquidity_len); - let mut collateral = Vec::with_capacity(usize::from(collateral_len)); - let mut liquidity = Vec::with_capacity(usize::from(liquidity_len)); + let mut collateral = Vec::with_capacity(collateral_len as usize); + let mut liquidity = Vec::with_capacity(liquidity_len as usize); let mut offset = 0; for _ in 0..collateral_len { @@ -430,8 +427,7 @@ impl Pack for Obligation { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), - // @FIXME: convert error to ProgramError - stale: bool::try_from(u8::from_le_bytes(*last_update_stale))?, + stale: unpack_bool(last_update_stale)?, }, lending_market: Pubkey::new_from_array(*lending_market), collateral, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 285ee76f48f..ea6b9451782 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -184,8 +184,7 @@ impl Reserve { }) } AmountType::PercentAmount => { - // @FIXME: convert error to ProgramError - let borrow_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); + let borrow_pct = Rate::from_percent(liquidity_amount as u8); let borrow_value = max_borrow_value.try_mul(borrow_pct)?; let borrow_amount = borrow_value .try_div(self.liquidity.median_price)? @@ -218,8 +217,7 @@ impl Reserve { let settle_amount = match liquidity_amount_type { AmountType::ExactAmount => Decimal::from(liquidity_amount).min(borrow_amount), AmountType::PercentAmount => { - // @FIXME: convert error to ProgramError - let settle_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); + let settle_pct = Rate::from_percent(liquidity_amount as u8); borrow_amount.try_mul(settle_pct)? } }; @@ -251,8 +249,7 @@ impl Reserve { Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) } AmountType::PercentAmount => { - // @FIXME: convert error to ProgramError - let liquidate_pct = Rate::from_percent(u8::try_from(liquidity_amount)?); + let liquidate_pct = Rate::from_percent(liquidity_amount as u8); liquidity.borrowed_amount_wads.try_mul(liquidate_pct)? } }; @@ -718,7 +715,7 @@ impl Pack for Reserve { ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); - *last_update_stale = u8::from(self.last_update.stale).to_le_bytes(); + pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); // liquidity @@ -749,7 +746,7 @@ impl Pack for Reserve { *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); - *collateral_enabled = u8::from(self.config.collateral_enabled).to_le_bytes(); + pack_bool(self.config.collateral_enabled, collateral_enabled); *borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); *host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); } @@ -816,8 +813,7 @@ impl Pack for Reserve { version: u8::from_le_bytes(*version), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), - // @FIXME: convert error to ProgramError - stale: bool::try_from(u8::from_le_bytes(*last_update_stale))?, + stale: unpack_bool(last_update_stale)?, }, lending_market: Pubkey::new_from_array(*lending_market), liquidity: ReserveLiquidity { @@ -842,8 +838,7 @@ impl Pack for Reserve { min_borrow_rate: u8::from_le_bytes(*min_borrow_rate), optimal_borrow_rate: u8::from_le_bytes(*optimal_borrow_rate), max_borrow_rate: u8::from_le_bytes(*max_borrow_rate), - // @FIXME: convert error to ProgramError - collateral_enabled: bool::try_from(u8::from_le_bytes(*collateral_enabled))?, + collateral_enabled: unpack_bool(collateral_enabled)?, fees: ReserveFees { borrow_fee_wad: u64::from_le_bytes(*borrow_fee_wad), host_fee_percentage: u8::from_le_bytes(*host_fee_percentage), From 8acbf213abb98df316991ed751220aec02e2942a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 16:43:21 -0500 Subject: [PATCH 102/191] fixing some mutable references --- token-lending/program/src/processor.rs | 59 +++++++++-------- token-lending/program/src/state/obligation.rs | 64 +++++++++++++++---- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 1cddce8cdf4..59d3208a3ba 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -165,7 +165,7 @@ fn process_init_lending_market( return Err(LendingError::InvalidTokenOwner.into()); } - let mut lending_market = LendingMarket { + let lending_market = LendingMarket { version: PROGRAM_VERSION, bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, owner: lending_market_owner, @@ -816,9 +816,9 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let collateral = obligation.find_or_add_collateral(*deposit_reserve_info.key)?; - - collateral.deposit(collateral_amount)?; + obligation + .find_or_add_collateral(*deposit_reserve_info.key)? + .deposit(collateral_amount)?; // @TODO: instead of always marking stale, we could look for an optional // reserve_liquidity_aggregator_info and update the market value. // then the depositor could borrow in the same transaction. @@ -919,7 +919,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let collateral = &mut obligation.find_collateral(*withdraw_reserve_info.key)?; + let collateral = obligation.find_collateral(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -980,17 +980,19 @@ fn process_withdraw_obligation_collateral( withdraw_amount } }; + if withdraw_amount == 0 { + return Err(LendingError::WithdrawTooSmall.into()); + } let obligation_token_amount = collateral .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; - - if withdraw_amount == 0 || obligation_token_amount == 0 { + if obligation_token_amount == 0 { return Err(LendingError::WithdrawTooSmall.into()); } - // @TODO: should withdraw_reserve.collateral.mint_total_supply change? - // if so, withdraw reserve needs to be writable. - collateral.withdraw(withdraw_amount)?; + obligation + .find_collateral_mut(*withdraw_reserve_info.key)? + .withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1095,8 +1097,6 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let liquidity = obligation.find_or_add_liquidity(*borrow_reserve_info.key)?; - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -1131,11 +1131,13 @@ fn process_borrow_obligation_liquidity( } borrow_reserve.liquidity.borrow(borrow_amount)?; - liquidity.borrow(borrow_amount)?; - obligation.last_update.mark_stale(); + Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; + obligation + .find_or_add_liquidity(*borrow_reserve_info.key)? + .borrow(borrow_amount)?; + obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; let mut owner_fee = borrow_fee; if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { @@ -1249,7 +1251,7 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let liquidity = &mut obligation.find_liquidity(*repay_reserve_info.key)?; + let liquidity = obligation.find_liquidity(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1275,10 +1277,13 @@ fn process_repay_obligation_liquidity( } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - liquidity.repay(settle_amount); + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + + obligation + .find_liquidity_mut(*repay_reserve_info.key)? + .repay(settle_amount); obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), @@ -1399,8 +1404,8 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let liquidity = &mut obligation.find_liquidity(*repay_reserve_info.key)?; - let collateral = &mut obligation.find_collateral(*withdraw_reserve_info.key)?; + let liquidity = obligation.find_liquidity(*repay_reserve_info.key)?; + let collateral = obligation.find_collateral(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1435,13 +1440,15 @@ fn process_liquidate_obligation( } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - liquidity.repay(settle_amount); - // @TODO: should withdraw_reserve.collateral.mint_total_supply change? - // if so, withdraw reserve needs to be writable. - collateral.withdraw(withdraw_amount); - obligation.last_update.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + + obligation + .find_liquidity_mut(*repay_reserve_info.key)? + .repay(settle_amount); + obligation + .find_collateral_mut(*withdraw_reserve_info.key)? + .withdraw(withdraw_amount); + obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ce8000fd8ba..3184da5c937 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -61,7 +61,7 @@ impl Obligation { /// Calculate the collateral market value pub fn collateral_value(&self) -> Result { let mut collateral_value = Decimal::zero(); - for collateral in self.collateral { + for collateral in &self.collateral { collateral_value = collateral_value.try_add(collateral.market_value)?; } Ok(collateral_value) @@ -72,7 +72,7 @@ impl Obligation { /// Calculate the liquidity market value pub fn liquidity_value(&self) -> Result { let mut liquidity_value = Decimal::zero(); - for liquidity in self.liquidity { + for liquidity in &self.liquidity { liquidity_value = liquidity_value.try_add(liquidity.market_value)?; } Ok(liquidity_value) @@ -124,7 +124,26 @@ impl Obligation { if let Some(collateral) = self .collateral .iter() - .find(|&collateral| collateral.deposit_reserve == deposit_reserve) + .find(|collateral| collateral.deposit_reserve == deposit_reserve) + { + Ok(collateral) + } else { + Err(LendingError::InvalidObligationCollateral.into()) + } + } + + /// Find collateral by deposit reserve and borrow a mutable reference to it + pub fn find_collateral_mut( + &mut self, + deposit_reserve: Pubkey, + ) -> Result<&mut ObligationCollateral, ProgramError> { + if self.collateral.len() == 0 { + return Err(LendingError::ObligationCollateralEmpty.into()); + } + if let Some(collateral) = self + .collateral + .iter_mut() + .find(|collateral| collateral.deposit_reserve == deposit_reserve) { Ok(collateral) } else { @@ -140,15 +159,14 @@ impl Obligation { if let Some(collateral) = self .collateral .iter_mut() - .find(|&collateral| collateral.deposit_reserve == deposit_reserve) + .find(|collateral| collateral.deposit_reserve == deposit_reserve) { Ok(collateral) + } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { + Err(LendingError::ObligationReserveLimit.into()) } else { self.collateral .push(ObligationCollateral::new(deposit_reserve)); - if self.collateral.len() + self.liquidity.len() > MAX_OBLIGATION_RESERVES { - return Err(LendingError::ObligationReserveLimit.into()); - } Ok(self.collateral.last_mut().unwrap()) } } @@ -164,7 +182,26 @@ impl Obligation { if let Some(liquidity) = self .liquidity .iter() - .find(|&liquidity| liquidity.borrow_reserve == borrow_reserve) + .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) + { + Ok(liquidity) + } else { + Err(LendingError::InvalidObligationLiquidity.into()) + } + } + + /// Find liquidity by borrow reserve and borrow a mutable reference to it + pub fn find_liquidity_mut( + &mut self, + borrow_reserve: Pubkey, + ) -> Result<&mut ObligationLiquidity, ProgramError> { + if self.liquidity.len() == 0 { + return Err(LendingError::ObligationLiquidityEmpty.into()); + } + if let Some(liquidity) = self + .liquidity + .iter_mut() + .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) { Ok(liquidity) } else { @@ -180,15 +217,14 @@ impl Obligation { if let Some(liquidity) = self .liquidity .iter_mut() - .find(|&liquidity| liquidity.borrow_reserve == borrow_reserve) + .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) { Ok(liquidity) + } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { + Err(LendingError::ObligationReserveLimit.into()) } else { self.liquidity .push(ObligationLiquidity::new(borrow_reserve)); - if self.collateral.len() + self.liquidity.len() > MAX_OBLIGATION_RESERVES { - return Err(LendingError::ObligationReserveLimit.into()); - } Ok(self.liquidity.last_mut().unwrap()) } } @@ -345,7 +381,7 @@ impl Pack for Obligation { *liquidity_len = u8::try_from(self.liquidity.len()).unwrap().to_le_bytes(); let mut offset = 0; - for collateral in self.collateral { + for collateral in &self.collateral { let collateral_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] let (deposit_reserve, deposited_amount, market_value) = @@ -355,7 +391,7 @@ impl Pack for Obligation { pack_decimal(collateral.market_value, market_value); offset += OBLIGATION_COLLATERAL_LEN; } - for liquidity in self.liquidity { + for liquidity in &self.liquidity { let liquidity_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) = From 8aab061588d4c6c8616c3d611b8b074efd3a264a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 18:45:16 -0500 Subject: [PATCH 103/191] continuing work on mutable borrows --- token-lending/program/src/processor.rs | 4 +- token-lending/program/src/state/obligation.rs | 83 +++++++++++-------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 59d3208a3ba..4da1ce754c4 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -667,7 +667,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - for collateral in obligation.collateral.iter_mut() { + for collateral in &mut obligation.collateral { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -692,7 +692,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .try_mul(deposit_reserve.liquidity.median_price)?; } - for liquidity in obligation.liquidity.iter_mut() { + for liquidity in &mut obligation.liquidity { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 3184da5c937..db623cdf9f9 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -121,17 +121,22 @@ impl Obligation { if self.collateral.len() == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } - if let Some(collateral) = self - .collateral - .iter() - .find(|collateral| collateral.deposit_reserve == deposit_reserve) - { + if let Some(collateral) = self._find_collateral(deposit_reserve) { Ok(collateral) } else { Err(LendingError::InvalidObligationCollateral.into()) } } + fn _find_collateral(&self, deposit_reserve: Pubkey) -> Option<&ObligationCollateral> { + for collateral in &self.collateral { + if collateral.deposit_reserve == deposit_reserve { + return Some(collateral); + } + } + None + } + /// Find collateral by deposit reserve and borrow a mutable reference to it pub fn find_collateral_mut( &mut self, @@ -140,33 +145,37 @@ impl Obligation { if self.collateral.len() == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } - if let Some(collateral) = self - .collateral - .iter_mut() - .find(|collateral| collateral.deposit_reserve == deposit_reserve) - { + if let Some(collateral) = self._find_collateral_mut(deposit_reserve) { Ok(collateral) } else { Err(LendingError::InvalidObligationCollateral.into()) } } + fn _find_collateral_mut( + &mut self, + deposit_reserve: Pubkey, + ) -> Option<&mut ObligationCollateral> { + for collateral in &mut self.collateral { + if collateral.deposit_reserve == deposit_reserve { + return Some(collateral); + } + } + None + } + /// Find or add collateral by deposit reserve pub fn find_or_add_collateral( &mut self, deposit_reserve: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { - if let Some(collateral) = self - .collateral - .iter_mut() - .find(|collateral| collateral.deposit_reserve == deposit_reserve) - { + if let Some(collateral) = self._find_collateral_mut(deposit_reserve) { Ok(collateral) } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { - self.collateral - .push(ObligationCollateral::new(deposit_reserve)); + let collateral = ObligationCollateral::new(deposit_reserve); + self.collateral.push(collateral); Ok(self.collateral.last_mut().unwrap()) } } @@ -179,17 +188,22 @@ impl Obligation { if self.liquidity.len() == 0 { return Err(LendingError::ObligationLiquidityEmpty.into()); } - if let Some(liquidity) = self - .liquidity - .iter() - .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) - { + if let Some(liquidity) = self._find_liquidity(borrow_reserve) { Ok(liquidity) } else { Err(LendingError::InvalidObligationLiquidity.into()) } } + fn _find_liquidity(&self, borrow_reserve: Pubkey) -> Option<&ObligationLiquidity> { + for liquidity in &self.liquidity { + if liquidity.borrow_reserve == borrow_reserve { + return Some(liquidity); + } + } + None + } + /// Find liquidity by borrow reserve and borrow a mutable reference to it pub fn find_liquidity_mut( &mut self, @@ -198,33 +212,34 @@ impl Obligation { if self.liquidity.len() == 0 { return Err(LendingError::ObligationLiquidityEmpty.into()); } - if let Some(liquidity) = self - .liquidity - .iter_mut() - .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) - { + if let Some(liquidity) = self._find_liquidity_mut(borrow_reserve) { Ok(liquidity) } else { Err(LendingError::InvalidObligationLiquidity.into()) } } + fn _find_liquidity_mut(&mut self, borrow_reserve: Pubkey) -> Option<&mut ObligationLiquidity> { + for liquidity in &mut self.liquidity { + if liquidity.borrow_reserve == borrow_reserve { + return Some(liquidity); + } + } + None + } + /// Find or add liquidity by borrow reserve pub fn find_or_add_liquidity( &mut self, borrow_reserve: Pubkey, ) -> Result<&mut ObligationLiquidity, ProgramError> { - if let Some(liquidity) = self - .liquidity - .iter_mut() - .find(|liquidity| liquidity.borrow_reserve == borrow_reserve) - { + if let Some(liquidity) = self._find_liquidity_mut(borrow_reserve) { Ok(liquidity) } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { - self.liquidity - .push(ObligationLiquidity::new(borrow_reserve)); + let liquidity = ObligationLiquidity::new(borrow_reserve); + self.liquidity.push(liquidity); Ok(self.liquidity.last_mut().unwrap()) } } From ee1832211cc60f07d34ceaae3cc6b8997e461dce Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 19:02:06 -0500 Subject: [PATCH 104/191] doc comment --- token-lending/program/src/state/obligation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index db623cdf9f9..c127e38642e 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -16,7 +16,7 @@ use std::convert::{TryFrom, TryInto}; /// Max number of collateral and liquidity reserve accounts combined for an obligation pub const MAX_OBLIGATION_RESERVES: usize = 10; -/// Borrow obligation state +/// Lending market obligation state #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { /// Version of the struct From 9fc94df7907eb2aa353dea82d6ca9ff13ccf24b5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 19:11:20 -0500 Subject: [PATCH 105/191] fixme comments --- token-lending/program/src/processor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 4da1ce754c4..1f578d0343f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -990,6 +990,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } + // @FIXME: this is just a temporary hack to get a mutable borrow. obligation .find_collateral_mut(*withdraw_reserve_info.key)? .withdraw(withdraw_amount)?; @@ -1279,6 +1280,7 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + // @FIXME: this is just a temporary hack to get a mutable borrow. obligation .find_liquidity_mut(*repay_reserve_info.key)? .repay(settle_amount); @@ -1442,9 +1444,11 @@ fn process_liquidate_obligation( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + // @FIXME: this is just a temporary hack to get a mutable borrow. obligation .find_liquidity_mut(*repay_reserve_info.key)? .repay(settle_amount); + // @FIXME: this is just a temporary hack to get a mutable borrow. obligation .find_collateral_mut(*withdraw_reserve_info.key)? .withdraw(withdraw_amount); From 52dde7673d16ff482152594290c3f0cd6f40c9b2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 22:26:53 -0500 Subject: [PATCH 106/191] all clippy errors fixed! --- token-lending/program/src/processor.rs | 48 ++++--- .../program/src/state/last_update.rs | 2 +- token-lending/program/src/state/mod.rs | 2 + token-lending/program/src/state/obligation.rs | 119 ++++-------------- token-lending/program/src/state/reserve.rs | 2 + 5 files changed, 53 insertions(+), 120 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 1f578d0343f..bd5a4e269ac 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -288,7 +288,7 @@ fn process_init_reserve( let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { - if let Some(_) = account_info_iter.peek() { + if account_info_iter.peek().is_some() { msg!("Invalid reserve liquidity aggregator account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -421,7 +421,7 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro } reserve.liquidity.median_price = read_median(reserve_liquidity_aggregator_info)?.median; - } else if let Some(_) = account_info_iter.peek() { + } else if account_info_iter.peek().is_some() { msg!("Invalid reserve liquidity aggregator account"); return Err(LendingError::InvalidAccountInput.into()); } @@ -717,7 +717,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .try_mul(borrow_reserve.liquidity.median_price)?; } - if let Some(_) = account_info_iter.peek() { + if account_info_iter.peek().is_some() { msg!("Too many obligation collateral or liquidity accounts"); return Err(LendingError::InvalidAccountInput.into()); } @@ -919,7 +919,8 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let collateral = obligation.find_collateral(*withdraw_reserve_info.key)?; + let index = obligation.find_collateral_index(*withdraw_reserve_info.key)?; + let collateral = &obligation.collateral[index]; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -972,12 +973,10 @@ fn process_withdraw_obligation_collateral( let withdraw_value = max_withdraw_value .try_mul(withdraw_pct)? .min(collateral.market_value); - let withdraw_amount = withdraw_value + withdraw_value .try_div(collateral.market_value)? .try_mul(collateral.deposited_amount)? - .try_floor_u64()?; - - withdraw_amount + .try_floor_u64()? } }; if withdraw_amount == 0 { @@ -990,10 +989,8 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } - // @FIXME: this is just a temporary hack to get a mutable borrow. - obligation - .find_collateral_mut(*withdraw_reserve_info.key)? - .withdraw(withdraw_amount)?; + let collateral = &mut obligation.collateral[index]; + collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1252,7 +1249,8 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let liquidity = obligation.find_liquidity(*repay_reserve_info.key)?; + let index = obligation.find_liquidity_index(*repay_reserve_info.key)?; + let liquidity = &obligation.liquidity[index]; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1280,10 +1278,8 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - // @FIXME: this is just a temporary hack to get a mutable borrow. - obligation - .find_liquidity_mut(*repay_reserve_info.key)? - .repay(settle_amount); + let liquidity = &mut obligation.liquidity[index]; + liquidity.repay(settle_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1406,8 +1402,10 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let liquidity = obligation.find_liquidity(*repay_reserve_info.key)?; - let collateral = obligation.find_collateral(*withdraw_reserve_info.key)?; + let liquidity_index = obligation.find_liquidity_index(*repay_reserve_info.key)?; + let liquidity = &obligation.liquidity[liquidity_index]; + let collateral_index = obligation.find_collateral_index(*withdraw_reserve_info.key)?; + let collateral = &obligation.collateral[collateral_index]; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1444,14 +1442,10 @@ fn process_liquidate_obligation( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - // @FIXME: this is just a temporary hack to get a mutable borrow. - obligation - .find_liquidity_mut(*repay_reserve_info.key)? - .repay(settle_amount); - // @FIXME: this is just a temporary hack to get a mutable borrow. - obligation - .find_collateral_mut(*withdraw_reserve_info.key)? - .withdraw(withdraw_amount); + let liquidity = &mut obligation.liquidity[liquidity_index]; + liquidity.repay(settle_amount)?; + let collateral = &mut obligation.collateral[collateral_index]; + collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index ec4dd1b1da2..86a3097541f 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -47,7 +47,7 @@ impl LastUpdate { impl PartialEq for LastUpdate { fn eq(&self, other: &Self) -> bool { - &self.slot == &other.slot + self.slot == other.slot } } diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 5a50649c362..802dee56fb2 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -36,6 +36,7 @@ pub const SLOTS_PER_YEAR: u64 = // Helpers fn pack_coption_key(src: &COption, dst: &mut [u8; 4 + PUBKEY_BYTES]) { + #[allow(clippy::ptr_offset_with_cast)] let (tag, body) = mut_array_refs![dst, 4, PUBKEY_BYTES]; match src { COption::Some(key) => { @@ -49,6 +50,7 @@ fn pack_coption_key(src: &COption, dst: &mut [u8; 4 + PUBKEY_BYTES]) { } fn unpack_coption_key(src: &[u8; 4 + PUBKEY_BYTES]) -> Result, ProgramError> { + #[allow(clippy::ptr_offset_with_cast)] let (tag, body) = array_refs![src, 4, PUBKEY_BYTES]; match *tag { [0, 0, 0, 0] => Ok(COption::None), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index c127e38642e..6b28bc76da3 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -114,54 +114,13 @@ impl Obligation { } /// Find collateral by deposit reserve - pub fn find_collateral( - &self, - deposit_reserve: Pubkey, - ) -> Result<&ObligationCollateral, ProgramError> { - if self.collateral.len() == 0 { + pub fn find_collateral_index(&self, deposit_reserve: Pubkey) -> Result { + if self.collateral.is_empty() { return Err(LendingError::ObligationCollateralEmpty.into()); } - if let Some(collateral) = self._find_collateral(deposit_reserve) { - Ok(collateral) - } else { - Err(LendingError::InvalidObligationCollateral.into()) - } - } - - fn _find_collateral(&self, deposit_reserve: Pubkey) -> Option<&ObligationCollateral> { - for collateral in &self.collateral { - if collateral.deposit_reserve == deposit_reserve { - return Some(collateral); - } - } - None - } - - /// Find collateral by deposit reserve and borrow a mutable reference to it - pub fn find_collateral_mut( - &mut self, - deposit_reserve: Pubkey, - ) -> Result<&mut ObligationCollateral, ProgramError> { - if self.collateral.len() == 0 { - return Err(LendingError::ObligationCollateralEmpty.into()); - } - if let Some(collateral) = self._find_collateral_mut(deposit_reserve) { - Ok(collateral) - } else { - Err(LendingError::InvalidObligationCollateral.into()) - } - } - - fn _find_collateral_mut( - &mut self, - deposit_reserve: Pubkey, - ) -> Option<&mut ObligationCollateral> { - for collateral in &mut self.collateral { - if collateral.deposit_reserve == deposit_reserve { - return Some(collateral); - } - } - None + self._find_collateral_index(deposit_reserve) + // @TODO: .ok_or(LendingError::X) is used elsewhere, but causes a compiler error here? + .ok_or_else(|| LendingError::InvalidObligationCollateral.into()) } /// Find or add collateral by deposit reserve @@ -169,8 +128,8 @@ impl Obligation { &mut self, deposit_reserve: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { - if let Some(collateral) = self._find_collateral_mut(deposit_reserve) { - Ok(collateral) + if let Some(index) = self._find_collateral_index(deposit_reserve) { + Ok(&mut self.collateral[index]) } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { @@ -180,52 +139,20 @@ impl Obligation { } } - /// Find liquidity by borrow reserve - pub fn find_liquidity( - &self, - borrow_reserve: Pubkey, - ) -> Result<&ObligationLiquidity, ProgramError> { - if self.liquidity.len() == 0 { - return Err(LendingError::ObligationLiquidityEmpty.into()); - } - if let Some(liquidity) = self._find_liquidity(borrow_reserve) { - Ok(liquidity) - } else { - Err(LendingError::InvalidObligationLiquidity.into()) - } - } - - fn _find_liquidity(&self, borrow_reserve: Pubkey) -> Option<&ObligationLiquidity> { - for liquidity in &self.liquidity { - if liquidity.borrow_reserve == borrow_reserve { - return Some(liquidity); - } - } - None + fn _find_collateral_index(&self, deposit_reserve: Pubkey) -> Option { + self.collateral + .iter() + .position(|collateral| collateral.deposit_reserve == deposit_reserve) } - /// Find liquidity by borrow reserve and borrow a mutable reference to it - pub fn find_liquidity_mut( - &mut self, - borrow_reserve: Pubkey, - ) -> Result<&mut ObligationLiquidity, ProgramError> { - if self.liquidity.len() == 0 { + /// Find liquidity by borrow reserve + pub fn find_liquidity_index(&self, borrow_reserve: Pubkey) -> Result { + if self.liquidity.is_empty() { return Err(LendingError::ObligationLiquidityEmpty.into()); } - if let Some(liquidity) = self._find_liquidity_mut(borrow_reserve) { - Ok(liquidity) - } else { - Err(LendingError::InvalidObligationLiquidity.into()) - } - } - - fn _find_liquidity_mut(&mut self, borrow_reserve: Pubkey) -> Option<&mut ObligationLiquidity> { - for liquidity in &mut self.liquidity { - if liquidity.borrow_reserve == borrow_reserve { - return Some(liquidity); - } - } - None + self._find_liquidity_index(borrow_reserve) + // @TODO: .ok_or(LendingError::X) is used elsewhere, but causes a compiler error here? + .ok_or_else(|| LendingError::InvalidObligationLiquidity.into()) } /// Find or add liquidity by borrow reserve @@ -233,8 +160,8 @@ impl Obligation { &mut self, borrow_reserve: Pubkey, ) -> Result<&mut ObligationLiquidity, ProgramError> { - if let Some(liquidity) = self._find_liquidity_mut(borrow_reserve) { - Ok(liquidity) + if let Some(index) = self._find_liquidity_index(borrow_reserve) { + Ok(&mut self.liquidity[index]) } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { @@ -243,6 +170,12 @@ impl Obligation { Ok(self.liquidity.last_mut().unwrap()) } } + + fn _find_liquidity_index(&self, borrow_reserve: Pubkey) -> Option { + self.liquidity + .iter() + .position(|liquidity| liquidity.borrow_reserve == borrow_reserve) + } } impl Sealed for Obligation {} @@ -452,6 +385,7 @@ impl Pack for Obligation { let mut offset = 0; for _ in 0..collateral_len { let collateral_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] let (deposit_reserve, deposited_amount, market_value) = array_refs![collateral_flat, PUBKEY_BYTES, 8, 16]; collateral.push(ObligationCollateral { @@ -463,6 +397,7 @@ impl Pack for Obligation { } for _ in 0..liquidity_len { let liquidity_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) = array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16, 16]; liquidity.push(ObligationLiquidity { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index ea6b9451782..cdf50c3431e 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -208,6 +208,7 @@ impl Reserve { } } + /// Repay liquidity up to a maximum borrow amount pub fn repay_liquidity( &self, liquidity_amount: u64, @@ -659,6 +660,7 @@ impl Pack for Reserve { 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, From 832599a7a37f0ab5ce30629f6417efac7878f480 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 22:36:15 -0500 Subject: [PATCH 107/191] return collateral/liquidity and index together --- token-lending/program/src/instruction.rs | 5 +--- token-lending/program/src/processor.rs | 16 +++++-------- token-lending/program/src/state/obligation.rs | 24 ++++++++++++------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 15cf37da04c..d9e30bdfaa5 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -12,10 +12,7 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, sysvar, }; -use std::{ - convert::{TryInto}, - mem::size_of, -}; +use std::{convert::TryInto, mem::size_of}; /// Describe how an input amount should be treated #[derive(Clone, Copy, Debug, PartialEq, FromPrimitive, ToPrimitive)] diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index bd5a4e269ac..7fc92267619 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -919,8 +919,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let index = obligation.find_collateral_index(*withdraw_reserve_info.key)?; - let collateral = &obligation.collateral[index]; + let (collateral, collateral_index) = obligation.find_collateral(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -989,7 +988,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } - let collateral = &mut obligation.collateral[index]; + let collateral = &mut obligation.collateral[collateral_index]; collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1249,8 +1248,7 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let index = obligation.find_liquidity_index(*repay_reserve_info.key)?; - let liquidity = &obligation.liquidity[index]; + let (liquidity, liquidity_index) = obligation.find_liquidity(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1278,7 +1276,7 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let liquidity = &mut obligation.liquidity[index]; + let liquidity = &mut obligation.liquidity[liquidity_index]; liquidity.repay(settle_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1402,10 +1400,8 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let liquidity_index = obligation.find_liquidity_index(*repay_reserve_info.key)?; - let liquidity = &obligation.liquidity[liquidity_index]; - let collateral_index = obligation.find_collateral_index(*withdraw_reserve_info.key)?; - let collateral = &obligation.collateral[collateral_index]; + let (liquidity, liquidity_index) = obligation.find_liquidity(*repay_reserve_info.key)?; + let (collateral, collateral_index) = obligation.find_collateral(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 6b28bc76da3..7ed37f634b3 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -114,13 +114,17 @@ impl Obligation { } /// Find collateral by deposit reserve - pub fn find_collateral_index(&self, deposit_reserve: Pubkey) -> Result { + pub fn find_collateral( + &self, + deposit_reserve: Pubkey, + ) -> Result<(&ObligationCollateral, usize), ProgramError> { if self.collateral.is_empty() { return Err(LendingError::ObligationCollateralEmpty.into()); } - self._find_collateral_index(deposit_reserve) - // @TODO: .ok_or(LendingError::X) is used elsewhere, but causes a compiler error here? - .ok_or_else(|| LendingError::InvalidObligationCollateral.into()) + let index = self + ._find_collateral_index(deposit_reserve) + .ok_or(LendingError::InvalidObligationCollateral)?; + Ok((&self.collateral[index], index)) } /// Find or add collateral by deposit reserve @@ -146,13 +150,17 @@ impl Obligation { } /// Find liquidity by borrow reserve - pub fn find_liquidity_index(&self, borrow_reserve: Pubkey) -> Result { + pub fn find_liquidity( + &self, + borrow_reserve: Pubkey, + ) -> Result<(&ObligationLiquidity, usize), ProgramError> { if self.liquidity.is_empty() { return Err(LendingError::ObligationLiquidityEmpty.into()); } - self._find_liquidity_index(borrow_reserve) - // @TODO: .ok_or(LendingError::X) is used elsewhere, but causes a compiler error here? - .ok_or_else(|| LendingError::InvalidObligationLiquidity.into()) + let index = self + ._find_liquidity_index(borrow_reserve) + .ok_or(LendingError::InvalidObligationLiquidity)?; + Ok((&self.liquidity[index], index)) } /// Find or add liquidity by borrow reserve From a660f918975084662b670fc9c47410e91b21e47d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 23 Mar 2021 22:49:34 -0500 Subject: [PATCH 108/191] fixed doc comments --- token-lending/program/src/instruction.rs | 4 ++-- token-lending/program/src/state/lending_market.rs | 2 +- token-lending/program/src/state/reserve.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index d9e30bdfaa5..2e61e205702 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -38,7 +38,7 @@ pub enum LendingInstruction { InitLendingMarket { /// Owner authority which can add new reserves owner: Pubkey, - /// The ratio of the loan to the value of the collateral as a percent + /// The target ratio of an obligation's borrows to deposits as a percent loan_to_value_ratio: u8, /// The percent at which an obligation is considered unhealthy liquidation_threshold: u8, @@ -135,7 +135,7 @@ pub enum LendingInstruction { /// 7. `[signer]` User transfer authority ($authority) /// 8. `[]` Token program id RedeemReserveCollateral { - /// Amount of collateral to return in exchange for liquidity + /// Amount of collateral to redeem in exchange for liquidity collateral_amount: u64, }, diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index d303a97188e..c3a7f30bd02 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -17,7 +17,7 @@ pub struct LendingMarket { pub token_program_id: Pubkey, /// Quote currency token mint pub quote_token_mint: Pubkey, - /// The target ratio of an obligation's borrowed liquidity to deposited collateral as a percent + /// The target ratio of an obligation's borrows to deposits as a percent pub loan_to_value_ratio: u8, /// The percent at which an obligation is considered unhealthy pub liquidation_threshold: u8, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index cdf50c3431e..2dfe19e88f9 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -208,7 +208,7 @@ impl Reserve { } } - /// Repay liquidity up to a maximum borrow amount + /// Repay liquidity up to the borrowed amount pub fn repay_liquidity( &self, liquidity_amount: u64, From 746f38aab4ed3429c243c4e7d4d3b712db027fed Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 24 Mar 2021 13:39:22 -0500 Subject: [PATCH 109/191] addressing some of justin's review feedback --- token-lending/client/src/main.rs | 1 - token-lending/program/src/instruction.rs | 12 +++++------- token-lending/program/src/processor.rs | 11 ----------- token-lending/program/src/state/mod.rs | 2 -- token-lending/program/src/state/obligation.rs | 4 ++-- token-lending/program/src/state/reserve.rs | 6 ++++-- token-lending/program/tests/borrow.rs | 4 ---- 7 files changed, 11 insertions(+), 29 deletions(-) diff --git a/token-lending/client/src/main.rs b/token-lending/client/src/main.rs index 7113da27ef9..0585b9d8cc8 100644 --- a/token-lending/client/src/main.rs +++ b/token-lending/client/src/main.rs @@ -240,7 +240,6 @@ pub fn create_reserve( .get_minimum_balance_for_rent_exemption(Reserve::LEN) .unwrap(), Reserve::LEN as u64, - // @FIXME: shouldn't all references like this be spl_token::id()? &id(), ), ], diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 2e61e205702..9725f8edcfc 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -428,20 +428,18 @@ impl LendingInstruction { fn unpack_bool(input: &[u8]) -> Result<(bool, &[u8]), ProgramError> { if !input.is_empty() { - let (bool, rest) = input.split_at(1); - let bool = bool + let (byte, rest) = input.split_at(1); + let boolean = byte .get(..1) .and_then(|slice| slice.try_into().ok()) .map(u8::from_le_bytes) - .and_then(|bool| match bool { - // @TODO bool::try_from(u8).ok() fails with - // ^^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `bool` + .and_then(|uint| match uint { 0 => Some(false), 1 => Some(true), _ => None, }) .ok_or(LendingError::InstructionUnpackError)?; - Ok((bool, rest)) + Ok((boolean, rest)) } else { Err(LendingError::InstructionUnpackError.into()) } @@ -742,7 +740,7 @@ pub fn refresh_reserve( AccountMeta::new_readonly(sysvar::clock::id(), false), ]; if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { - accounts.push(AccountMeta::new(reserve_liquidity_aggregator_pubkey, false)); + accounts.push(AccountMeta::new_readonly(reserve_liquidity_aggregator_pubkey, false)); } Instruction { program_id, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7fc92267619..563b0e54a20 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -350,14 +350,6 @@ fn process_init_reserve( token_program: token_program_id.clone(), })?; - spl_token_init_account(TokenInitializeAccountParams { - account: reserve_liquidity_fee_receiver_info.clone(), - mint: reserve_liquidity_mint_info.clone(), - owner: lending_market_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - spl_token_init_mint(TokenInitializeMintParams { mint: reserve_collateral_mint_info.clone(), authority: lending_market_authority_info.key, @@ -819,9 +811,6 @@ fn process_deposit_obligation_collateral( obligation .find_or_add_collateral(*deposit_reserve_info.key)? .deposit(collateral_amount)?; - // @TODO: instead of always marking stale, we could look for an optional - // reserve_liquidity_aggregator_info and update the market value. - // then the depositor could borrow in the same transaction. obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 802dee56fb2..f4b95f7748f 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -75,8 +75,6 @@ fn pack_bool(bool: bool, dst: &mut [u8; 1]) { } fn unpack_bool(src: &[u8; 1]) -> Result { - // @TODO bool::try_from(u8).ok() fails with - // ^^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `bool` match u8::from_le_bytes(*src) { 0 => Ok(false), 1 => Ok(true), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 7ed37f634b3..f7e26dda069 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -387,8 +387,8 @@ impl Pack for Obligation { let collateral_len = u8::from_le_bytes(*collateral_len); let liquidity_len = u8::from_le_bytes(*liquidity_len); - let mut collateral = Vec::with_capacity(collateral_len as usize); - let mut liquidity = Vec::with_capacity(liquidity_len as usize); + let mut collateral = Vec::with_capacity(collateral_len as usize + 1); + let mut liquidity = Vec::with_capacity(liquidity_len as usize + 1); let mut offset = 0; for _ in 0..collateral_len { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 2dfe19e88f9..a4b8de50267 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -65,11 +65,13 @@ impl Reserve { .collateral_exchange_rate()? .liquidity_to_collateral(liquidity_amount)?; - self.liquidity + self.liquidity.available_amount = self + .liquidity .available_amount .checked_add(liquidity_amount) .ok_or(LendingError::MathOverflow)?; - self.collateral + self.collateral.mint_total_supply = self + .collateral .mint_total_supply .checked_add(collateral_amount) .ok_or(LendingError::MathOverflow)?; diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index fce7ff57753..7e04cbf1201 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -120,7 +120,6 @@ async fn test_borrow_quote_currency() { .unwrap() .0; - // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); @@ -158,7 +157,6 @@ async fn test_borrow_quote_currency() { assert!(total_fee > 0); assert!(host_fee > 0); - // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposited - total_fee); @@ -278,7 +276,6 @@ async fn test_borrow_base_currency() { .unwrap() .0; - // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); @@ -315,7 +312,6 @@ async fn test_borrow_base_currency() { assert!(total_fee > 0); assert!(host_fee > 0); - // @FIXME: fees let collateral_supply = get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); From 4781168ae4012e2ffc7d48c946da278984615d5c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 24 Mar 2021 13:56:22 -0500 Subject: [PATCH 110/191] collateral/liquidity -> deposits/borrows (in some cases) --- token-lending/program/src/processor.rs | 26 +-- token-lending/program/src/state/obligation.rs | 156 +++++++++--------- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 563b0e54a20..a4c66a785dc 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -659,7 +659,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - for collateral in &mut obligation.collateral { + for collateral in &mut obligation.deposits { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -684,7 +684,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .try_mul(deposit_reserve.liquidity.median_price)?; } - for liquidity in &mut obligation.liquidity { + for liquidity in &mut obligation.borrows { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); @@ -809,7 +809,7 @@ fn process_deposit_obligation_collateral( } obligation - .find_or_add_collateral(*deposit_reserve_info.key)? + .find_or_add_deposit(*deposit_reserve_info.key)? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -908,7 +908,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let (collateral, collateral_index) = obligation.find_collateral(*withdraw_reserve_info.key)?; + let (collateral, deposit_index) = obligation.find_deposit(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -949,7 +949,7 @@ fn process_withdraw_obligation_collateral( let withdraw_amount = collateral_amount.min(collateral.deposited_amount); let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = obligation.collateral_value()?.try_mul(withdraw_pct)?; + let withdraw_value = obligation.deposited_value()?.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { return Err(LendingError::WithdrawTooLarge.into()); } @@ -977,7 +977,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } - let collateral = &mut obligation.collateral[collateral_index]; + let collateral = &mut obligation.deposits[deposit_index]; collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1120,7 +1120,7 @@ fn process_borrow_obligation_liquidity( Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; obligation - .find_or_add_liquidity(*borrow_reserve_info.key)? + .find_or_add_borrow(*borrow_reserve_info.key)? .borrow(borrow_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1237,7 +1237,7 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let (liquidity, liquidity_index) = obligation.find_liquidity(*repay_reserve_info.key)?; + let (liquidity, borrow_index) = obligation.find_borrow(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1265,7 +1265,7 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let liquidity = &mut obligation.liquidity[liquidity_index]; + let liquidity = &mut obligation.borrows[borrow_index]; liquidity.repay(settle_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1389,8 +1389,8 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let (liquidity, liquidity_index) = obligation.find_liquidity(*repay_reserve_info.key)?; - let (collateral, collateral_index) = obligation.find_collateral(*withdraw_reserve_info.key)?; + let (liquidity, borrow_index) = obligation.find_borrow(*repay_reserve_info.key)?; + let (collateral, deposit_index) = obligation.find_deposit(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1427,9 +1427,9 @@ fn process_liquidate_obligation( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let liquidity = &mut obligation.liquidity[liquidity_index]; + let liquidity = &mut obligation.borrows[borrow_index]; + let collateral = &mut obligation.deposits[deposit_index]; liquidity.repay(settle_amount)?; - let collateral = &mut obligation.collateral[collateral_index]; collateral.withdraw(withdraw_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index f7e26dda069..ebc4639a4aa 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -25,10 +25,10 @@ pub struct Obligation { pub last_update: LastUpdate, /// Lending market address pub lending_market: Pubkey, - /// Collateral state for the obligation, unique by deposit reserve address - pub collateral: Vec, - /// Liquidity state for the obligation, unique by borrow reserve address - pub liquidity: Vec, + /// 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, } /// Create new obligation @@ -51,53 +51,53 @@ impl Obligation { version: PROGRAM_VERSION, last_update: LastUpdate::new(current_slot), lending_market, - collateral: vec![], - liquidity: vec![], + deposits: vec![], + borrows: vec![], } } // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, // but that seems sloppy. - /// Calculate the collateral market value - pub fn collateral_value(&self) -> Result { - let mut collateral_value = Decimal::zero(); - for collateral in &self.collateral { - collateral_value = collateral_value.try_add(collateral.market_value)?; + /// Calculate the deposited collateral market value + pub fn deposited_value(&self) -> Result { + let mut deposited_value = Decimal::zero(); + for collateral in &self.deposits { + deposited_value = deposited_value.try_add(collateral.market_value)?; } - Ok(collateral_value) + Ok(deposited_value) } // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, // but that seems sloppy. - /// Calculate the liquidity market value - pub fn liquidity_value(&self) -> Result { - let mut liquidity_value = Decimal::zero(); - for liquidity in &self.liquidity { - liquidity_value = liquidity_value.try_add(liquidity.market_value)?; + /// Calculate the borrowed liquidity market value + pub fn borrowed_value(&self) -> Result { + let mut borrowed_value = Decimal::zero(); + for liquidity in &self.borrows { + borrowed_value = borrowed_value.try_add(liquidity.market_value)?; } - Ok(liquidity_value) + Ok(borrowed_value) } /// Calculate the ratio of liquidity market value to collateral market value pub fn loan_to_value(&self) -> Result { - let collateral_value = self.collateral_value()?; - if collateral_value == Decimal::zero() { + let deposited_value = self.deposited_value()?; + if deposited_value == Decimal::zero() { return Err(LendingError::ObligationCollateralEmpty.into()); } - Rate::try_from(self.liquidity_value()?.try_div(collateral_value)?) + Rate::try_from(self.borrowed_value()?.try_div(deposited_value)?) } /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio pub fn max_withdraw_value(&self, loan_to_value_ratio: Rate) -> Result { - let min_collateral_value = self.liquidity_value()?.try_div(loan_to_value_ratio)?; - self.collateral_value()?.try_sub(min_collateral_value) + let min_deposited_value = self.borrowed_value()?.try_div(loan_to_value_ratio)?; + self.deposited_value()?.try_sub(min_deposited_value) } /// Calculate the maximum liquidity value that can be borrowed for a given loan to value ratio pub fn max_borrow_value(&self, loan_to_value_ratio: Rate) -> Result { - self.collateral_value()? + self.deposited_value()? .try_mul(loan_to_value_ratio)? - .try_sub(self.liquidity_value()?) + .try_sub(self.borrowed_value()?) } /// Calculate the maximum liquidation amount for a given liquidity @@ -106,7 +106,7 @@ impl Obligation { liquidity: &ObligationLiquidity, ) -> Result { let max_liquidation_value = self - .liquidity_value()? + .borrowed_value()? .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? .min(liquidity.market_value); let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; @@ -114,73 +114,73 @@ impl Obligation { } /// Find collateral by deposit reserve - pub fn find_collateral( + pub fn find_deposit( &self, deposit_reserve: Pubkey, ) -> Result<(&ObligationCollateral, usize), ProgramError> { - if self.collateral.is_empty() { + if self.deposits.is_empty() { return Err(LendingError::ObligationCollateralEmpty.into()); } - let index = self - ._find_collateral_index(deposit_reserve) + let deposit_index = self + ._find_deposit_index(deposit_reserve) .ok_or(LendingError::InvalidObligationCollateral)?; - Ok((&self.collateral[index], index)) + Ok((&self.deposits[deposit_index], deposit_index)) } /// Find or add collateral by deposit reserve - pub fn find_or_add_collateral( + pub fn find_or_add_deposit( &mut self, deposit_reserve: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { - if let Some(index) = self._find_collateral_index(deposit_reserve) { - Ok(&mut self.collateral[index]) - } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { + if let Some(deposit_index) = self._find_deposit_index(deposit_reserve) { + Ok(&mut self.deposits[deposit_index]) + } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { let collateral = ObligationCollateral::new(deposit_reserve); - self.collateral.push(collateral); - Ok(self.collateral.last_mut().unwrap()) + self.deposits.push(collateral); + Ok(self.deposits.last_mut().unwrap()) } } - fn _find_collateral_index(&self, deposit_reserve: Pubkey) -> Option { - self.collateral + fn _find_deposit_index(&self, deposit_reserve: Pubkey) -> Option { + self.deposits .iter() .position(|collateral| collateral.deposit_reserve == deposit_reserve) } /// Find liquidity by borrow reserve - pub fn find_liquidity( + pub fn find_borrow( &self, borrow_reserve: Pubkey, ) -> Result<(&ObligationLiquidity, usize), ProgramError> { - if self.liquidity.is_empty() { + if self.borrows.is_empty() { return Err(LendingError::ObligationLiquidityEmpty.into()); } - let index = self - ._find_liquidity_index(borrow_reserve) + let borrow_index = self + ._find_borrow_index(borrow_reserve) .ok_or(LendingError::InvalidObligationLiquidity)?; - Ok((&self.liquidity[index], index)) + Ok((&self.borrows[borrow_index], borrow_index)) } /// Find or add liquidity by borrow reserve - pub fn find_or_add_liquidity( + pub fn find_or_add_borrow( &mut self, borrow_reserve: Pubkey, ) -> Result<&mut ObligationLiquidity, ProgramError> { - if let Some(index) = self._find_liquidity_index(borrow_reserve) { - Ok(&mut self.liquidity[index]) - } else if self.collateral.len() + self.liquidity.len() >= MAX_OBLIGATION_RESERVES { + if let Some(borrow_index) = self._find_borrow_index(borrow_reserve) { + Ok(&mut self.borrows[borrow_index]) + } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { let liquidity = ObligationLiquidity::new(borrow_reserve); - self.liquidity.push(liquidity); - Ok(self.liquidity.last_mut().unwrap()) + self.borrows.push(liquidity); + Ok(self.borrows.last_mut().unwrap()) } } - fn _find_liquidity_index(&self, borrow_reserve: Pubkey) -> Option { - self.liquidity + fn _find_borrow_index(&self, borrow_reserve: Pubkey) -> Option { + self.borrows .iter() .position(|liquidity| liquidity.borrow_reserve == borrow_reserve) } @@ -315,8 +315,8 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, - collateral_len, - liquidity_len, + deposits_len, + borrows_len, data_flat, ) = mut_array_refs![ output, @@ -333,25 +333,25 @@ impl Pack for Obligation { *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()); - *collateral_len = u8::try_from(self.collateral.len()).unwrap().to_le_bytes(); - *liquidity_len = u8::try_from(self.liquidity.len()).unwrap().to_le_bytes(); + *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; - for collateral in &self.collateral { - let collateral_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + 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) = - mut_array_refs![collateral_flat, PUBKEY_BYTES, 8, 16]; + mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; 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; } - for liquidity in &self.liquidity { - let liquidity_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; + 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) = - mut_array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16, 16]; + mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref()); pack_decimal( liquidity.cumulative_borrow_rate_wads, @@ -371,8 +371,8 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, - collateral_len, - liquidity_len, + deposits_len, + borrows_len, data_flat, ) = array_refs![ input, @@ -385,30 +385,30 @@ impl Pack for Obligation { OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; - let collateral_len = u8::from_le_bytes(*collateral_len); - let liquidity_len = u8::from_le_bytes(*liquidity_len); - let mut collateral = Vec::with_capacity(collateral_len as usize + 1); - let mut liquidity = Vec::with_capacity(liquidity_len as usize + 1); + 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..collateral_len { - let collateral_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + 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) = - array_refs![collateral_flat, PUBKEY_BYTES, 8, 16]; - collateral.push(ObligationCollateral { + array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; + deposits.push(ObligationCollateral { deposit_reserve: Pubkey::new(deposit_reserve), deposited_amount: u64::from_le_bytes(*deposited_amount), market_value: unpack_decimal(market_value), }); offset += OBLIGATION_COLLATERAL_LEN; } - for _ in 0..liquidity_len { - let liquidity_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_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) = - array_refs![liquidity_flat, PUBKEY_BYTES, 16, 16, 16]; - liquidity.push(ObligationLiquidity { + array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; + borrows.push(ObligationLiquidity { borrow_reserve: Pubkey::new(borrow_reserve), cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), borrowed_amount_wads: unpack_decimal(borrowed_amount_wads), @@ -424,8 +424,8 @@ impl Pack for Obligation { stale: unpack_bool(last_update_stale)?, }, lending_market: Pubkey::new_from_array(*lending_market), - collateral, - liquidity, + deposits, + borrows, }) } } From 621b01c75d5d6d534c593b1a63eb716be7c25094 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 24 Mar 2021 14:04:29 -0500 Subject: [PATCH 111/191] add periods to make doc comment display format consistent --- token-lending/program/src/instruction.rs | 208 ++++++++++++----------- 1 file changed, 105 insertions(+), 103 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 9725f8edcfc..28f00183f95 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -61,23 +61,23 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account + /// 0. `[writable]` Source liquidity token account. /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination collateral token account - uninitialized - /// 2. `[writable]` Reserve account - uninitialized - /// 3. `[]` Reserve liquidity SPL Token mint - /// 4. `[writable]` Reserve liquidity supply SPL Token account - uninitialized - /// 5. `[writable]` Reserve liquidity fee receiver - uninitialized - /// 6. `[writable]` Reserve collateral SPL Token mint - uninitialized - /// 7. `[writable]` Reserve collateral token supply - uninitialized - /// 8. `[]` Lending market account - /// 9. `[signer]` Lending market owner - /// 10 `[]` Derived lending market authority - /// 11 `[]` User transfer authority ($authority) - /// 12 `[]` Clock sysvar - /// 13 `[]` Rent sysvar - /// 14 `[]` Token program id - /// 15 `[optional]` Reserve liquidity aggregator account + /// 1. `[writable]` Destination collateral token account - uninitialized. + /// 2. `[writable]` Reserve account - uninitialized. + /// 3. `[]` Reserve liquidity SPL Token mint. + /// 4. `[writable]` Reserve liquidity supply SPL Token account - uninitialized. + /// 5. `[writable]` Reserve liquidity fee receiver - uninitialized. + /// 6. `[writable]` Reserve collateral SPL Token mint - uninitialized. + /// 7. `[writable]` Reserve collateral token supply - uninitialized. + /// 8. `[]` Lending market account. + /// 9. `[signer]` Lending market owner. + /// 10 `[]` Derived lending market authority. + /// 11 `[]` User transfer authority ($authority). + /// 12 `[]` Clock sysvar. + /// 13 `[]` Rent sysvar. + /// 14 `[]` Token program id. + /// 15 `[optional]` Reserve liquidity aggregator account. /// Not required for quote currency reserves. /// Must match quote and base currency. InitReserve { @@ -92,9 +92,11 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Reserve account - /// 1. `[]` Clock sysvar - /// 2. `[optional]` Reserve liquidity aggregator account + /// 0. `[writable]` Reserve account. + /// 1. `[]` Clock sysvar. + /// 2. `[optional]` Reserve liquidity aggregator account. + /// Required if the reserve currency is not the lending market quote + /// currency. RefreshReserve, // 4 @@ -103,17 +105,17 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account + /// 0. `[writable]` Source liquidity token account. /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination collateral token account - /// 2. `[writable]` Reserve account - /// 3. `[writable]` Reserve liquidity supply SPL Token account - /// 4. `[writable]` Reserve collateral SPL Token mint - /// 5. `[]` Lending market account - /// 6. `[]` Derived lending market authority - /// 7. `[signer]` User transfer authority ($authority) - /// 8. `[]` Clock sysvar - /// 9. `[]` Token program id + /// 1. `[writable]` Destination collateral token account. + /// 2. `[writable]` Reserve account. + /// 3. `[writable]` Reserve liquidity supply SPL Token account. + /// 4. `[writable]` Reserve collateral SPL Token mint. + /// 5. `[]` Lending market account. + /// 6. `[]` Derived lending market authority. + /// 7. `[signer]` User transfer authority ($authority). + /// 8. `[]` Clock sysvar. + /// 9. `[]` Token program id. DepositReserveLiquidity { /// Amount of liquidity to deposit in exchange for collateral liquidity_amount: u64, @@ -124,16 +126,16 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source collateral token account + /// 0. `[writable]` Source collateral token account. /// $authority can transfer $collateral_amount. - /// 1. `[writable]` Destination liquidity token account - /// 2. `[writable]` Reserve account - /// 3. `[writable]` Reserve collateral SPL Token mint - /// 4. `[writable]` Reserve liquidity supply SPL Token account - /// 5. `[]` Lending market account - /// 6. `[]` Derived lending market authority - /// 7. `[signer]` User transfer authority ($authority) - /// 8. `[]` Token program id + /// 1. `[writable]` Destination liquidity token account. + /// 2. `[writable]` Reserve account. + /// 3. `[writable]` Reserve collateral SPL Token mint. + /// 4. `[writable]` Reserve liquidity supply SPL Token account. + /// 5. `[]` Lending market account. + /// 6. `[]` Derived lending market authority. + /// 7. `[signer]` User transfer authority ($authority). + /// 8. `[]` Token program id. RedeemReserveCollateral { /// Amount of collateral to redeem in exchange for liquidity collateral_amount: u64, @@ -144,11 +146,11 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation account - uninitialized - /// 1. `[]` Lending market account - /// 2. `[]` Clock sysvar - /// 3. `[]` Rent sysvar - /// 4. `[]` Token program id + /// 0. `[writable]` Obligation account - uninitialized. + /// 1. `[]` Lending market account. + /// 2. `[]` Clock sysvar. + /// 3. `[]` Rent sysvar. + /// 4. `[]` Token program id. InitObligation, // 7 @@ -158,12 +160,12 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation account - /// 1. `[]` Lending market account - /// 2. `[]` Clock sysvar - /// 3. `[]` Token program id - /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order - /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order + /// 0. `[writable]` Obligation account. + /// 1. `[]` Lending market account. + /// 2. `[]` Clock sysvar. + /// 3. `[]` Token program id. + /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. + /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. RefreshObligation, // 8 @@ -171,19 +173,19 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source collateral token account + /// 0. `[writable]` Source collateral token account. /// Minted by deposit reserve collateral mint. /// $authority can transfer $collateral_amount. - /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account - /// 2. `[]` Deposit reserve account - refreshed - /// 3. `[writable]` Obligation account - /// 4. `[writable]` Obligation token mint - /// 5. `[writable]` Obligation token output account - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[signer]` User transfer authority ($authority) - /// 9. `[]` Clock sysvar - /// 10 `[]` Token program id + /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. + /// 2. `[]` Deposit reserve account - refreshed. + /// 3. `[writable]` Obligation account. + /// 4. `[writable]` Obligation token mint. + /// 5. `[writable]` Obligation token output account. + /// 6. `[]` Lending market account. + /// 7. `[]` Derived lending market authority. + /// 8. `[signer]` User transfer authority ($authority). + /// 9. `[]` Clock sysvar. + /// 10 `[]` Token program id. DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, @@ -194,19 +196,19 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account - /// 1. `[writable]` Destination collateral token account + /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. + /// 1. `[writable]` Destination collateral token account. /// Minted by withdraw reserve collateral mint. /// $authority can transfer $collateral_amount. - /// 2. `[]` Withdraw reserve account - refreshed - /// 3. `[writable]` Obligation account - refreshed - /// 4. `[writable]` Obligation token mint - /// 5. `[writable]` Obligation token input account - /// 6. `[]` Lending market account - /// 7. `[]` Derived lending market authority - /// 8. `[signer]` User transfer authority ($authority) - /// 9. `[]` Clock sysvar - /// 10 `[]` Token program id + /// 2. `[]` Withdraw reserve account - refreshed. + /// 3. `[writable]` Obligation account - refreshed. + /// 4. `[writable]` Obligation token mint. + /// 5. `[writable]` Obligation token input account. + /// 6. `[]` Lending market account. + /// 7. `[]` Derived lending market authority. + /// 8. `[signer]` User transfer authority ($authority). + /// 9. `[]` Clock sysvar. + /// 10 `[]` Token program id. WithdrawObligationCollateral { /// Amount of collateral to withdraw - usage depends on `collateral_amount_type` collateral_amount: u64, @@ -220,18 +222,18 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account - /// 1. `[writable]` Destination liquidity token account + /// 0. `[writable]` Source borrow reserve liquidity supply SPL Token account. + /// 1. `[writable]` Destination liquidity token account. /// Minted by borrow reserve liquidity mint. - /// 2. `[writable]` Borrow reserve account - refreshed - /// 3. `[writable]` Borrow reserve liquidity fee receiver account + /// 2. `[writable]` Borrow reserve account - refreshed. + /// 3. `[writable]` Borrow reserve liquidity fee receiver account. /// Must be the fee account specified at InitReserve. - /// 4. `[writable]` Obligation account - refreshed - /// 5. `[]` Lending market account - /// 6. `[]` Derived lending market authority - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program id - /// 9. `[optional, writable]` Host fee receiver account + /// 4. `[writable]` Obligation account - refreshed. + /// 5. `[]` Lending market account. + /// 6. `[]` Derived lending market authority. + /// 7. `[]` Clock sysvar. + /// 8. `[]` Token program id. + /// 9. `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -245,17 +247,17 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account + /// 0. `[writable]` Source liquidity token account. /// Minted by repay reserve liquidity mint. /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account - /// 2. `[writable]` Repay reserve account - refreshed - /// 3. `[writable]` Obligation account - refreshed - /// 4. `[]` Lending market account - /// 5. `[]` Derived lending market authority - /// 6. `[signer]` User transfer authority ($authority) - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program id + /// 1. `[writable]` Destination repay reserve liquidity supply SPL Token account. + /// 2. `[writable]` Repay reserve account - refreshed. + /// 3. `[writable]` Obligation account - refreshed. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[signer]` User transfer authority ($authority). + /// 7. `[]` Clock sysvar. + /// 8. `[]` Token program id. RepayObligationLiquidity { /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, @@ -269,21 +271,21 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Source liquidity token account + /// 0. `[writable]` Source liquidity token account. /// Minted by repay reserve liquidity mint. /// $authority can transfer $liquidity_amount. - /// 1. `[writable]` Destination collateral token account - /// Minted by withdraw reserve collateral mint - /// 2. `[writable]` Repay reserve account - refreshed - /// 3. `[writable]` Repay reserve liquidity supply SPL Token account - /// 4. `[writable]` Withdraw reserve account - refreshed - /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account - /// 6. `[writable]` Obligation account - refreshed - /// 7. `[]` Lending market account - /// 8. `[]` Derived lending market authority - /// 9. `[signer]` User transfer authority ($authority) - /// 10 `[]` Clock sysvar - /// 11 `[]` Token program id + /// 1. `[writable]` Destination collateral token account. + /// Minted by withdraw reserve collateral mint. + /// 2. `[writable]` Repay reserve account - refreshed. + /// 3. `[writable]` Repay reserve liquidity supply SPL Token account. + /// 4. `[writable]` Withdraw reserve account - refreshed. + /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account. + /// 6. `[writable]` Obligation account - refreshed. + /// 7. `[]` Lending market account. + /// 8. `[]` Derived lending market authority. + /// 9. `[signer]` User transfer authority ($authority). + /// 10 `[]` Clock sysvar. + /// 11 `[]` Token program id. LiquidateObligation { /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` liquidity_amount: u64, From 6f3c0e11d456b50b450e97361977465846820785 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 24 Mar 2021 14:16:20 -0500 Subject: [PATCH 112/191] reuse allocated structs and inline construction --- token-lending/program/src/processor.rs | 68 +++++++-------- token-lending/program/src/state/obligation.rs | 24 ------ token-lending/program/src/state/reserve.rs | 83 ------------------- 3 files changed, 35 insertions(+), 140 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index a4c66a785dc..74e1691d503 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,14 +1,13 @@ //! Program state processor -use crate::state::NewReserveLiquidityParams; use crate::{ error::LendingError, instruction::{AmountType, LendingInstruction}, math::{Decimal, Rate, TryDiv, TryMul, WAD}, state::{ - BorrowLiquidityResult, LendingMarket, LiquidateObligationResult, NewObligationParams, - NewReserveParams, Obligation, RepayLiquidityResult, Reserve, ReserveCollateral, - ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, + BorrowLiquidityResult, LastUpdate, LendingMarket, LiquidateObligationResult, Obligation, + RepayLiquidityResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, + PROGRAM_VERSION, }, }; use flux_aggregator::read_median; @@ -158,22 +157,22 @@ fn process_init_lending_market( let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, lending_market_info)?; - assert_uninitialized::(lending_market_info)?; + let mut lending_market = assert_uninitialized::(lending_market_info)?; unpack_mint("e_token_mint_info.data.borrow())?; if quote_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } - let lending_market = LendingMarket { - version: PROGRAM_VERSION, - bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, - owner: lending_market_owner, - quote_token_mint: *quote_token_mint_info.key, - token_program_id: *token_program_id.key, - loan_to_value_ratio, - liquidation_threshold, - }; + lending_market.version = PROGRAM_VERSION; + lending_market.bump_seed = + Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1; + lending_market.owner = lending_market_owner; + lending_market.quote_token_mint = *quote_token_mint_info.key; + lending_market.token_program_id = *token_program_id.key; + lending_market.loan_to_value_ratio = loan_to_value_ratio; + lending_market.liquidation_threshold = liquidation_threshold; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) @@ -260,7 +259,7 @@ fn process_init_reserve( let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, reserve_info)?; - assert_uninitialized::(reserve_info)?; + let mut reserve = assert_uninitialized::(reserve_info)?; assert_uninitialized::(reserve_liquidity_supply_info)?; assert_uninitialized::(reserve_liquidity_fee_receiver_info)?; assert_uninitialized::(reserve_collateral_mint_info)?; @@ -320,25 +319,27 @@ fn process_init_reserve( return Err(LendingError::InvalidTokenOwner.into()); } - let reserve_liquidity = ReserveLiquidity::new(NewReserveLiquidityParams { + reserve.version = PROGRAM_VERSION; + reserve.last_update = LastUpdate::new(clock.slot); + reserve.lending_market = *lending_market_info.key; + reserve.liquidity = ReserveLiquidity { mint_pubkey: *reserve_liquidity_mint_info.key, mint_decimals: reserve_liquidity_mint.decimals, supply_pubkey: *reserve_liquidity_supply_info.key, fee_receiver: *reserve_liquidity_fee_receiver_info.key, aggregator: reserve_liquidity_aggregator, + cumulative_borrow_rate_wads: Decimal::one(), median_price: reserve_liquidity_median_price, - }); - let reserve_collateral = ReserveCollateral::new( - *reserve_collateral_mint_info.key, - *reserve_collateral_supply_info.key, - ); - let mut reserve = Reserve::new(NewReserveParams { - current_slot: clock.slot, - lending_market: *lending_market_info.key, - liquidity: reserve_liquidity, - collateral: reserve_collateral, - config, - }); + available_amount: 0, + borrowed_amount_wads: Decimal::zero(), + }; + reserve.collateral = ReserveCollateral { + mint_pubkey: *reserve_collateral_mint_info.key, + mint_total_supply: 0, + supply_pubkey: *reserve_collateral_supply_info.key, + }; + reserve.config = config; + let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; @@ -616,7 +617,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, obligation_info)?; - assert_uninitialized::(obligation_info)?; + let mut obligation = assert_uninitialized::(obligation_info)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { @@ -626,10 +627,11 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } - let obligation = Obligation::new(NewObligationParams { - current_slot: clock.slot, - lending_market: *lending_market_info.key, - }); + obligation.version = PROGRAM_VERSION; + obligation.last_update = LastUpdate::new(clock.slot); + obligation.lending_market = *lending_market_info.key; + obligation.deposits = vec![]; + obligation.borrows = vec![]; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ebc4639a4aa..508ab7e997c 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -31,31 +31,7 @@ pub struct Obligation { pub borrows: Vec, } -/// Create new obligation -pub struct NewObligationParams { - /// Current slot - pub current_slot: Slot, - /// Lending market address - pub lending_market: Pubkey, -} - impl Obligation { - /// Create new obligation - pub fn new(params: NewObligationParams) -> Self { - let NewObligationParams { - current_slot, - lending_market, - } = params; - - Self { - version: PROGRAM_VERSION, - last_update: LastUpdate::new(current_slot), - lending_market, - deposits: vec![], - borrows: vec![], - } - } - // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, // but that seems sloppy. /// Calculate the deposited collateral market value diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index a4b8de50267..99728789156 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -39,26 +39,6 @@ pub struct Reserve { } impl Reserve { - /// Initialize new reserve state - pub fn new(params: NewReserveParams) -> Self { - let NewReserveParams { - current_slot, - lending_market, - liquidity, - collateral, - config, - } = params; - - Self { - version: PROGRAM_VERSION, - last_update: LastUpdate::new(current_slot), - lending_market, - liquidity, - collateral, - config, - } - } - /// 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 @@ -307,20 +287,6 @@ impl Reserve { } } -/// Create new reserve -pub struct NewReserveParams { - /// Current slot - pub current_slot: Slot, - /// Lending market address - pub lending_market: Pubkey, - /// Reserve collateral - pub collateral: ReserveCollateral, - /// Reserve liquidity - pub liquidity: ReserveLiquidity, - /// Reserve configuration values - pub config: ReserveConfig, -} - /// Borrow liquidity result #[derive(Debug)] pub struct BorrowLiquidityResult { @@ -379,29 +345,6 @@ pub struct ReserveLiquidity { } impl ReserveLiquidity { - /// Initialize new reserve state - pub fn new(params: NewReserveLiquidityParams) -> Self { - let NewReserveLiquidityParams { - mint_pubkey, - mint_decimals, - supply_pubkey, - fee_receiver, - aggregator, - median_price, - } = params; - Self { - mint_pubkey, - mint_decimals, - supply_pubkey, - fee_receiver, - aggregator, - cumulative_borrow_rate_wads: Decimal::one(), - median_price, - available_amount: 0, - borrowed_amount_wads: Decimal::zero(), - } - } - /// 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) @@ -464,23 +407,6 @@ impl ReserveLiquidity { } } -/// Reserve liquidity -#[derive(Clone, Debug, Default, PartialEq)] -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 fee receiver address - pub fee_receiver: Pubkey, - /// Reserve liquidity aggregator state account - pub aggregator: COption, - /// Reserve liquidity median price - pub median_price: u64, -} - /// Reserve collateral #[derive(Clone, Debug, Default, PartialEq)] pub struct ReserveCollateral { @@ -493,15 +419,6 @@ pub struct ReserveCollateral { } impl ReserveCollateral { - /// New reserve collateral info - pub fn new(mint_pubkey: Pubkey, supply_pubkey: Pubkey) -> Self { - Self { - mint_pubkey, - mint_total_supply: 0, - supply_pubkey, - } - } - /// Return the current collateral exchange rate. fn exchange_rate( &self, From 4b301b1abd2beae128a9c698a528f3a2e9c5f523 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 12:57:07 -0500 Subject: [PATCH 113/191] clarify todo comment --- token-lending/program/src/processor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 74e1691d503..fd88ab83345 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -771,6 +771,7 @@ fn process_deposit_obligation_collateral( } // @TODO: is this necessary? we don't care about market price or interest here yet. // however, we will if we add the ability to deposit and borrow in one transaction. + // it would also be important if deposit_reserve.config.collateral_enabled changes. if deposit_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } From d07b5f8f0cccd8d4f4e04f4d7b847c5ad8a79fcd Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 14:39:20 -0500 Subject: [PATCH 114/191] add new/init functions --- token-lending/program/src/instruction.rs | 5 +- token-lending/program/src/processor.rs | 73 +++++++++-------- .../program/src/state/lending_market.rs | 36 +++++++++ token-lending/program/src/state/obligation.rs | 27 +++++++ token-lending/program/src/state/reserve.rs | 80 +++++++++++++++++++ 5 files changed, 183 insertions(+), 38 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 28f00183f95..798d14b81d9 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -742,7 +742,10 @@ pub fn refresh_reserve( AccountMeta::new_readonly(sysvar::clock::id(), false), ]; if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { - accounts.push(AccountMeta::new_readonly(reserve_liquidity_aggregator_pubkey, false)); + accounts.push(AccountMeta::new_readonly( + reserve_liquidity_aggregator_pubkey, + false, + )); } Instruction { program_id, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index fd88ab83345..d80fc15b536 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -5,9 +5,10 @@ use crate::{ instruction::{AmountType, LendingInstruction}, math::{Decimal, Rate, TryDiv, TryMul, WAD}, state::{ - BorrowLiquidityResult, LastUpdate, LendingMarket, LiquidateObligationResult, Obligation, - RepayLiquidityResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, - PROGRAM_VERSION, + BorrowLiquidityResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, + LendingMarket, LiquidateObligationResult, NewReserveCollateralParams, + NewReserveLiquidityParams, Obligation, RepayLiquidityResult, Reserve, ReserveCollateral, + ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, }, }; use flux_aggregator::read_median; @@ -136,7 +137,7 @@ pub fn process_instruction( fn process_init_lending_market( program_id: &Pubkey, - lending_market_owner: Pubkey, + owner: Pubkey, loan_to_value_ratio: u8, liquidation_threshold: u8, accounts: &[AccountInfo], @@ -164,15 +165,14 @@ fn process_init_lending_market( return Err(LendingError::InvalidTokenOwner.into()); } - lending_market.version = PROGRAM_VERSION; - lending_market.bump_seed = - Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1; - lending_market.owner = lending_market_owner; - lending_market.quote_token_mint = *quote_token_mint_info.key; - lending_market.token_program_id = *token_program_id.key; - lending_market.loan_to_value_ratio = loan_to_value_ratio; - lending_market.liquidation_threshold = liquidation_threshold; - + lending_market.init(InitLendingMarketParams { + bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, + owner, + quote_token_mint: *quote_token_mint_info.key, + token_program_id: *token_program_id.key, + loan_to_value_ratio, + liquidation_threshold, + }); LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) @@ -320,25 +320,23 @@ fn process_init_reserve( } reserve.version = PROGRAM_VERSION; - reserve.last_update = LastUpdate::new(clock.slot); - reserve.lending_market = *lending_market_info.key; - reserve.liquidity = ReserveLiquidity { - mint_pubkey: *reserve_liquidity_mint_info.key, - mint_decimals: reserve_liquidity_mint.decimals, - supply_pubkey: *reserve_liquidity_supply_info.key, - fee_receiver: *reserve_liquidity_fee_receiver_info.key, - aggregator: reserve_liquidity_aggregator, - cumulative_borrow_rate_wads: Decimal::one(), - median_price: reserve_liquidity_median_price, - available_amount: 0, - borrowed_amount_wads: Decimal::zero(), - }; - reserve.collateral = ReserveCollateral { - mint_pubkey: *reserve_collateral_mint_info.key, - mint_total_supply: 0, - supply_pubkey: *reserve_collateral_supply_info.key, - }; - reserve.config = config; + reserve.init(InitReserveParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, + liquidity: ReserveLiquidity::new(NewReserveLiquidityParams { + mint_pubkey: *reserve_liquidity_mint_info.key, + mint_decimals: reserve_liquidity_mint.decimals, + supply_pubkey: *reserve_liquidity_supply_info.key, + fee_receiver: *reserve_liquidity_fee_receiver_info.key, + aggregator: reserve_liquidity_aggregator, + median_price: reserve_liquidity_median_price, + }), + collateral: ReserveCollateral::new(NewReserveCollateralParams { + mint_pubkey: *reserve_collateral_mint_info.key, + supply_pubkey: *reserve_collateral_supply_info.key, + }), + config, + }); let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; @@ -627,11 +625,12 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } - obligation.version = PROGRAM_VERSION; - obligation.last_update = LastUpdate::new(clock.slot); - obligation.lending_market = *lending_market_info.key; - obligation.deposits = vec![]; - obligation.borrows = vec![]; + obligation.init(InitObligationParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, + deposits: Vec::with_capacity(0), + borrows: Vec::with_capacity(0), + }); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index c3a7f30bd02..793610e297a 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -25,6 +25,42 @@ pub struct LendingMarket { pub owner: Pubkey, } +/// Initialize a lending market +pub struct InitLendingMarketParams { + /// Bump seed for derived authority address + pub bump_seed: u8, + /// Token program id + pub token_program_id: Pubkey, + /// Quote currency token mint + pub quote_token_mint: Pubkey, + /// The target ratio of an obligation's borrows to deposits as a percent + pub loan_to_value_ratio: u8, + /// The percent at which an obligation is considered unhealthy + pub liquidation_threshold: u8, + /// Owner authority which can add new reserves + pub owner: 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_token_mint = params.quote_token_mint; + self.token_program_id = params.token_program_id; + self.loan_to_value_ratio = params.loan_to_value_ratio; + self.liquidation_threshold = params.liquidation_threshold; + } +} + impl Sealed for LendingMarket {} impl IsInitialized for LendingMarket { fn is_initialized(&self) -> bool { diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 508ab7e997c..2d3ecdbfc5c 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -31,7 +31,34 @@ pub struct Obligation { pub borrows: Vec, } +/// 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, + /// 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 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.deposits = params.deposits; + self.borrows = params.borrows; + } + // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, // but that seems sloppy. /// Calculate the deposited collateral market value diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 99728789156..9223136d6bf 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -26,6 +26,7 @@ pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2; pub struct Reserve { /// Version of the struct pub version: u8, + // @TODO: check to see if `last_update` is changed when supply changes /// Last slot when supply and rates updated pub last_update: LastUpdate, /// Lending market address @@ -38,7 +39,38 @@ pub struct Reserve { pub config: ReserveConfig, } +/// 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, +} + 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; + } + /// 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 @@ -344,7 +376,38 @@ pub struct ReserveLiquidity { pub borrowed_amount_wads: Decimal, } +/// 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 fee receiver address + pub fee_receiver: Pubkey, + /// Optional reserve liquidity aggregator state account + pub aggregator: COption, + /// Reserve liquidity median price in quote currency + pub median_price: u64, +} + 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, + fee_receiver: params.fee_receiver, + aggregator: params.aggregator, + cumulative_borrow_rate_wads: Decimal::one(), + median_price: params.median_price, + available_amount: 0, + borrowed_amount_wads: Decimal::zero(), + } + } + /// 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) @@ -418,7 +481,24 @@ pub struct ReserveCollateral { pub supply_pubkey: Pubkey, } +/// Create a new reserve collateral +pub struct NewReserveCollateralParams { + /// Reserve collateral mint address + pub mint_pubkey: Pubkey, + /// 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, + } + } + /// Return the current collateral exchange rate. fn exchange_rate( &self, From 7b0cfeede4b5c9f45b26857baaa9db257fe36ec6 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 15:32:53 -0500 Subject: [PATCH 115/191] AmountType::Percent -> u64::MAX --- token-lending/program/src/instruction.rs | 119 ++++--------------- token-lending/program/src/processor.rs | 126 ++++----------------- token-lending/program/src/state/reserve.rs | 107 ++++++++--------- 3 files changed, 91 insertions(+), 261 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 798d14b81d9..021f94e2e64 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -4,8 +4,6 @@ use crate::{ error::LendingError, state::{ReserveConfig, ReserveFees}, }; -use num_derive::{FromPrimitive, ToPrimitive}; -use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, @@ -14,15 +12,6 @@ use solana_program::{ }; use std::{convert::TryInto, mem::size_of}; -/// Describe how an input amount should be treated -#[derive(Clone, Copy, Debug, PartialEq, FromPrimitive, ToPrimitive)] -pub enum AmountType { - /// Treat amount as an exact amount of tokens - ExactAmount, - /// Treat amount as a percentage of available tokens - PercentAmount, -} - /// Instructions supported by the lending program. #[derive(Clone, Debug, PartialEq)] pub enum LendingInstruction { @@ -210,10 +199,8 @@ pub enum LendingInstruction { /// 9. `[]` Clock sysvar. /// 10 `[]` Token program id. WithdrawObligationCollateral { - /// Amount of collateral to withdraw - usage depends on `collateral_amount_type` + /// Amount of collateral to withdraw - u64::MAX for up to 100% of deposited amount collateral_amount: u64, - /// Describe how `collateral_amount` should be treated - collateral_amount_type: AmountType, }, // 10 @@ -235,10 +222,8 @@ pub enum LendingInstruction { /// 8. `[]` Token program id. /// 9. `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { - /// Amount of liquidity to borrow - usage depends on `liquidity_amount_type` + /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, // @TODO: slippage constraint - https://git.io/JmV67 }, @@ -259,10 +244,8 @@ pub enum LendingInstruction { /// 7. `[]` Clock sysvar. /// 8. `[]` Token program id. RepayObligationLiquidity { - /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` + /// Amount of liquidity to repay - u64::MAX for 100% of borrowed amount liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, }, // 12 @@ -287,10 +270,8 @@ pub enum LendingInstruction { /// 10 `[]` Clock sysvar. /// 11 `[]` Token program id. LiquidateObligation { - /// Amount of liquidity to repay - usage depends on `liquidity_amount_type` + /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount liquidity_amount: u64, - /// Describe how `liquidity_amount` should be treated - liquidity_amount_type: AmountType, }, } @@ -357,44 +338,20 @@ impl LendingInstruction { Self::DepositObligationCollateral { collateral_amount } } 9 => { - let (collateral_amount, rest) = Self::unpack_u64(rest)?; - let (collateral_amount_type, _rest) = Self::unpack_u8(rest)?; - let collateral_amount_type = AmountType::from_u8(collateral_amount_type) - .ok_or(LendingError::InstructionUnpackError)?; - Self::WithdrawObligationCollateral { - collateral_amount, - collateral_amount_type, - } + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::WithdrawObligationCollateral { collateral_amount } } 10 => { - let (liquidity_amount, rest) = Self::unpack_u64(rest)?; - let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; - let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) - .ok_or(LendingError::InstructionUnpackError)?; - Self::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } + let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + Self::BorrowObligationLiquidity { liquidity_amount } } 11 => { - let (liquidity_amount, rest) = Self::unpack_u64(rest)?; - let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; - let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) - .ok_or(LendingError::InstructionUnpackError)?; - Self::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } + let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + Self::RepayObligationLiquidity { liquidity_amount } } 12 => { - let (liquidity_amount, rest) = Self::unpack_u64(rest)?; - let (liquidity_amount_type, _rest) = Self::unpack_u8(rest)?; - let liquidity_amount_type = AmountType::from_u8(liquidity_amount_type) - .ok_or(LendingError::InstructionUnpackError)?; - Self::LiquidateObligation { - liquidity_amount, - liquidity_amount_type, - } + let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + Self::LiquidateObligation { liquidity_amount } } _ => return Err(LendingError::InstructionUnpackError.into()), }) @@ -524,37 +481,21 @@ impl LendingInstruction { buf.push(8); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::WithdrawObligationCollateral { - collateral_amount, - collateral_amount_type, - } => { + Self::WithdrawObligationCollateral { collateral_amount } => { buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); - buf.extend_from_slice(&collateral_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + Self::BorrowObligationLiquidity { liquidity_amount } => { buf.push(10); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + Self::RepayObligationLiquidity { liquidity_amount } => { buf.push(11); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } - Self::LiquidateObligation { - liquidity_amount, - liquidity_amount_type, - } => { + Self::LiquidateObligation { liquidity_amount } => { buf.push(12); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - buf.extend_from_slice(&liquidity_amount_type.to_u8().unwrap().to_le_bytes()); } } buf @@ -842,7 +783,6 @@ pub fn deposit_obligation_collateral( pub fn withdraw_obligation_collateral( program_id: Pubkey, collateral_amount: u64, - collateral_amount_type: AmountType, source_collateral_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, withdraw_reserve_pubkey: Pubkey, @@ -871,11 +811,7 @@ pub fn withdraw_obligation_collateral( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::WithdrawObligationCollateral { - collateral_amount, - collateral_amount_type, - } - .pack(), + data: LendingInstruction::WithdrawObligationCollateral { collateral_amount }.pack(), } } @@ -884,7 +820,6 @@ pub fn withdraw_obligation_collateral( pub fn borrow_obligation_liquidity( program_id: Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, destination_liquidity_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, @@ -914,11 +849,7 @@ pub fn borrow_obligation_liquidity( Instruction { program_id, accounts, - data: LendingInstruction::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } - .pack(), + data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), } } @@ -927,7 +858,6 @@ pub fn borrow_obligation_liquidity( pub fn repay_obligation_liquidity( program_id: Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, destination_liquidity_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, @@ -952,11 +882,7 @@ pub fn repay_obligation_liquidity( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } - .pack(), + data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), } } @@ -965,7 +891,6 @@ pub fn repay_obligation_liquidity( pub fn liquidate_obligation( program_id: Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, source_liquidity_pubkey: Pubkey, destination_collateral_pubkey: Pubkey, repay_reserve_pubkey: Pubkey, @@ -996,10 +921,6 @@ pub fn liquidate_obligation( AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::LiquidateObligation { - liquidity_amount, - liquidity_amount_type, - } - .pack(), + data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index d80fc15b536..bb07a4dc8c2 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -2,7 +2,7 @@ use crate::{ error::LendingError, - instruction::{AmountType, LendingInstruction}, + instruction::LendingInstruction, math::{Decimal, Rate, TryDiv, TryMul, WAD}, state::{ BorrowLiquidityResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, @@ -84,53 +84,21 @@ pub fn process_instruction( msg!("Instruction: Deposit Obligation Collateral"); process_deposit_obligation_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::WithdrawObligationCollateral { - collateral_amount, - collateral_amount_type, - } => { + LendingInstruction::WithdrawObligationCollateral { collateral_amount } => { msg!("Instruction: Withdraw Obligation Collateral"); - process_withdraw_obligation_collateral( - program_id, - collateral_amount, - collateral_amount_type, - accounts, - ) + process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::BorrowObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + LendingInstruction::BorrowObligationLiquidity { liquidity_amount } => { msg!("Instruction: Borrow Obligation Liquidity"); - process_borrow_obligation_liquidity( - program_id, - liquidity_amount, - liquidity_amount_type, - accounts, - ) + process_borrow_obligation_liquidity(program_id, liquidity_amount, accounts) } - LendingInstruction::RepayObligationLiquidity { - liquidity_amount, - liquidity_amount_type, - } => { + LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { msg!("Instruction: Repay Obligation Liquidity"); - process_repay_obligation_liquidity( - program_id, - liquidity_amount, - liquidity_amount_type, - accounts, - ) + process_repay_obligation_liquidity(program_id, liquidity_amount, accounts) } - LendingInstruction::LiquidateObligation { - liquidity_amount, - liquidity_amount_type, - } => { + LendingInstruction::LiquidateObligation { liquidity_amount } => { msg!("Instruction: Liquidate Obligation"); - process_liquidate_obligation( - program_id, - liquidity_amount, - liquidity_amount_type, - accounts, - ) + process_liquidate_obligation(program_id, liquidity_amount, accounts) } } } @@ -841,18 +809,11 @@ fn process_deposit_obligation_collateral( fn process_withdraw_obligation_collateral( program_id: &Pubkey, collateral_amount: u64, - collateral_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { return Err(LendingError::InvalidAmount.into()); } - if let AmountType::PercentAmount = collateral_amount_type { - if collateral_amount > 100 { - msg!("Collateral amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; @@ -946,28 +907,21 @@ fn process_withdraw_obligation_collateral( let max_withdraw_value = obligation.max_withdraw_value(loan_to_value_ratio)?; - let withdraw_amount = match collateral_amount_type { - AmountType::ExactAmount => { - let withdraw_amount = collateral_amount.min(collateral.deposited_amount); - let withdraw_pct = - Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = obligation.deposited_value()?.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - return Err(LendingError::WithdrawTooLarge.into()); - } - - withdraw_amount - } - AmountType::PercentAmount => { - let withdraw_pct = Rate::from_percent(collateral_amount as u8); - let withdraw_value = max_withdraw_value - .try_mul(withdraw_pct)? - .min(collateral.market_value); - withdraw_value - .try_div(collateral.market_value)? - .try_mul(collateral.deposited_amount)? - .try_floor_u64()? + let withdraw_amount = if collateral_amount == u64::MAX { + let withdraw_value = max_withdraw_value.min(collateral.market_value); + withdraw_value + .try_div(collateral.market_value)? + .try_mul(collateral.deposited_amount)? + .try_floor_u64()? + .min(collateral.deposited_amount) + } else { + let withdraw_amount = collateral_amount.min(collateral.deposited_amount); + let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + let withdraw_value = obligation.deposited_value()?.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + return Err(LendingError::WithdrawTooLarge.into()); } + withdraw_amount }; if withdraw_amount == 0 { return Err(LendingError::WithdrawTooSmall.into()); @@ -1009,18 +963,11 @@ fn process_withdraw_obligation_collateral( fn process_borrow_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; @@ -1108,11 +1055,7 @@ fn process_borrow_obligation_liquidity( receive_amount, borrow_fee, host_fee, - } = borrow_reserve.borrow_liquidity( - liquidity_amount, - liquidity_amount_type, - max_borrow_value, - )?; + } = borrow_reserve.borrow_liquidity(liquidity_amount, max_borrow_value)?; if receive_amount == 0 { return Err(LendingError::BorrowTooSmall.into()); @@ -1171,18 +1114,11 @@ fn process_borrow_obligation_liquidity( fn process_repay_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; @@ -1254,11 +1190,7 @@ fn process_repay_obligation_liquidity( let RepayLiquidityResult { settle_amount, repay_amount, - } = repay_reserve.repay_liquidity( - liquidity_amount, - liquidity_amount_type, - liquidity.borrowed_amount_wads, - )?; + } = repay_reserve.repay_liquidity(liquidity_amount, liquidity.borrowed_amount_wads)?; if repay_amount == 0 { return Err(LendingError::RepayTooSmall.into()); @@ -1288,18 +1220,11 @@ fn process_repay_obligation_liquidity( fn process_liquidate_obligation( program_id: &Pubkey, liquidity_amount: u64, - liquidity_amount_type: AmountType, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { return Err(LendingError::InvalidAmount.into()); } - if let AmountType::PercentAmount = liquidity_amount_type { - if liquidity_amount > 100 { - msg!("Liquidity amount must be in range (0, 100]"); - return Err(LendingError::InvalidAmount.into()); - } - } let account_info_iter = &mut accounts.iter(); let source_liquidity_info = next_account_info(account_info_iter)?; @@ -1416,7 +1341,6 @@ fn process_liquidate_obligation( withdraw_amount, } = withdraw_reserve.liquidate_obligation( liquidity_amount, - liquidity_amount_type, &obligation, &liquidity, &collateral, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 9223136d6bf..404472560e4 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -1,7 +1,6 @@ use super::*; use crate::{ error::LendingError, - instruction::AmountType, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; @@ -172,53 +171,47 @@ impl Reserve { pub fn borrow_liquidity( &self, liquidity_amount: u64, - liquidity_amount_type: AmountType, max_borrow_value: Decimal, ) -> Result { - match liquidity_amount_type { - AmountType::ExactAmount => { - let receive_amount = liquidity_amount; - 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 = borrow_amount.try_mul(self.liquidity.median_price)?; - if borrow_value > max_borrow_value { - return Err(LendingError::BorrowTooLarge.into()); - } - - Ok(BorrowLiquidityResult { - borrow_amount, - receive_amount, - borrow_fee, - host_fee, - }) - } - AmountType::PercentAmount => { - let borrow_pct = Rate::from_percent(liquidity_amount as u8); - let borrow_value = max_borrow_value.try_mul(borrow_pct)?; - let borrow_amount = borrow_value - .try_div(self.liquidity.median_price)? - .min(self.liquidity.available_amount.into()); - let (origination_fee, host_fee) = self - .config - .fees - .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?; - let receive_amount = borrow_amount - .try_floor_u64()? - .checked_sub(origination_fee) - .ok_or(LendingError::MathOverflow)?; - - Ok(BorrowLiquidityResult { - borrow_amount, - receive_amount, - borrow_fee: origination_fee, - host_fee, - }) + if liquidity_amount == u64::MAX { + let borrow_amount = max_borrow_value + .try_div(self.liquidity.median_price)? + .min(self.liquidity.available_amount.into()); + let (origination_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?; + let receive_amount = borrow_amount + .try_floor_u64()? + .checked_sub(origination_fee) + .ok_or(LendingError::MathOverflow)?; + + Ok(BorrowLiquidityResult { + borrow_amount, + receive_amount, + borrow_fee: origination_fee, + host_fee, + }) + } else { + let receive_amount = liquidity_amount; + 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 = borrow_amount.try_mul(self.liquidity.median_price)?; + if borrow_value > max_borrow_value { + return Err(LendingError::BorrowTooLarge.into()); } + + Ok(BorrowLiquidityResult { + borrow_amount, + receive_amount, + borrow_fee, + host_fee, + }) } } @@ -226,15 +219,12 @@ impl Reserve { pub fn repay_liquidity( &self, liquidity_amount: u64, - liquidity_amount_type: AmountType, borrow_amount: Decimal, ) -> Result { - let settle_amount = match liquidity_amount_type { - AmountType::ExactAmount => Decimal::from(liquidity_amount).min(borrow_amount), - AmountType::PercentAmount => { - let settle_pct = Rate::from_percent(liquidity_amount as u8); - borrow_amount.try_mul(settle_pct)? - } + let settle_amount = if liquidity_amount == u64::MAX { + borrow_amount + } else { + Decimal::from(liquidity_amount).min(borrow_amount) }; let repay_amount = if settle_amount == borrow_amount { settle_amount.try_ceil_u64()? @@ -252,21 +242,16 @@ impl Reserve { pub fn liquidate_obligation( &self, liquidity_amount: u64, - liquidity_amount_type: AmountType, obligation: &Obligation, liquidity: &ObligationLiquidity, collateral: &ObligationCollateral, ) -> Result { let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; - let liquidate_amount = match liquidity_amount_type { - AmountType::ExactAmount => { - Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) - } - AmountType::PercentAmount => { - let liquidate_pct = Rate::from_percent(liquidity_amount as u8); - liquidity.borrowed_amount_wads.try_mul(liquidate_pct)? - } + let liquidate_amount = if liquidity_amount == u64::MAX { + liquidity.borrowed_amount_wads + } else { + Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) }; let (settle_amount, settle_pct, repay_amount) = From 7cc02aacde45896b162ffb568dba64a335d6640c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 15:45:09 -0500 Subject: [PATCH 116/191] match rounding to what's used in Reserve::borrow_liquidity --- token-lending/program/src/state/reserve.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 404472560e4..b090f21b5dd 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -400,15 +400,14 @@ impl ReserveLiquidity { /// Subtract borrow amount from available liquidity and add to borrows pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { - // @TODO: should this be rounded differently? - let borrow_rounded = borrow_amount.try_ceil_u64()?; - if borrow_rounded > self.available_amount { + let receive_amount = borrow_amount.try_floor_u64()?; + if receive_amount > self.available_amount { return Err(LendingError::InsufficientLiquidity.into()); } self.available_amount = self .available_amount - .checked_sub(borrow_rounded) + .checked_sub(receive_amount) .ok_or(LendingError::MathOverflow)?; self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?; From b137101a04539bd01ff4663c7d9f8c63041dc13d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 17:21:56 -0500 Subject: [PATCH 117/191] remove collateral/liquidity from deposits/borrows if zeroed out --- token-lending/program/src/processor.rs | 12 ++++------ token-lending/program/src/state/obligation.rs | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index bb07a4dc8c2..9258c9dca8b 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -933,8 +933,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } - let collateral = &mut obligation.deposits[deposit_index]; - collateral.withdraw(withdraw_amount)?; + obligation.withdraw(withdraw_amount, deposit_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1199,8 +1198,7 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let liquidity = &mut obligation.borrows[borrow_index]; - liquidity.repay(settle_amount)?; + obligation.repay(settle_amount, borrow_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1353,10 +1351,8 @@ fn process_liquidate_obligation( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let liquidity = &mut obligation.borrows[borrow_index]; - let collateral = &mut obligation.deposits[deposit_index]; - liquidity.repay(settle_amount)?; - collateral.withdraw(withdraw_amount)?; + obligation.repay(settle_amount, borrow_index)?; + obligation.withdraw(withdraw_amount, deposit_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 2d3ecdbfc5c..da859685257 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -59,6 +59,28 @@ impl Obligation { self.borrows = params.borrows; } + /// Withdraw collateral and remove it from deposits if zeroed out + pub fn withdraw(&mut self, withdraw_amount: u64, deposit_index: usize) -> ProgramResult { + let collateral = &mut self.deposits[deposit_index]; + if withdraw_amount == collateral.deposited_amount { + self.deposits.remove(deposit_index); + } else { + collateral.withdraw(withdraw_amount)?; + } + Ok(()) + } + + /// Repay liquidity and remove it from borrows if zeroed out + pub fn repay(&mut self, settle_amount: Decimal, borrow_index: usize) -> ProgramResult { + let liquidity = &mut self.borrows[borrow_index]; + if settle_amount == liquidity.borrowed_amount_wads { + self.borrows.remove(borrow_index); + } else { + liquidity.repay(settle_amount)?; + } + Ok(()) + } + // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, // but that seems sloppy. /// Calculate the deposited collateral market value From 8bd4e2033f7e9d4362336f52be29e67859990fd2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 25 Mar 2021 17:36:10 -0500 Subject: [PATCH 118/191] more verbose naming --- token-lending/program/src/processor.rs | 20 ++++----- token-lending/program/src/state/obligation.rs | 44 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 9258c9dca8b..636edef6b5a 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -779,7 +779,7 @@ fn process_deposit_obligation_collateral( } obligation - .find_or_add_deposit(*deposit_reserve_info.key)? + .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -871,7 +871,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let (collateral, deposit_index) = obligation.find_deposit(*withdraw_reserve_info.key)?; + let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } @@ -933,7 +933,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooSmall.into()); } - obligation.withdraw(withdraw_amount, deposit_index)?; + obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1064,7 +1064,7 @@ fn process_borrow_obligation_liquidity( Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; obligation - .find_or_add_borrow(*borrow_reserve_info.key)? + .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key)? .borrow(borrow_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1174,7 +1174,7 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let (liquidity, borrow_index) = obligation.find_borrow(*repay_reserve_info.key)?; + let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1198,7 +1198,7 @@ fn process_repay_obligation_liquidity( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - obligation.repay(settle_amount, borrow_index)?; + obligation.repay(settle_amount, liquidity_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1314,8 +1314,8 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let (liquidity, borrow_index) = obligation.find_borrow(*repay_reserve_info.key)?; - let (collateral, deposit_index) = obligation.find_deposit(*withdraw_reserve_info.key)?; + let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1351,8 +1351,8 @@ fn process_liquidate_obligation( repay_reserve.liquidity.repay(repay_amount, settle_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - obligation.repay(settle_amount, borrow_index)?; - obligation.withdraw(withdraw_amount, deposit_index)?; + obligation.repay(settle_amount, liquidity_index)?; + obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index da859685257..eb43362d8d6 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -60,10 +60,10 @@ impl Obligation { } /// Withdraw collateral and remove it from deposits if zeroed out - pub fn withdraw(&mut self, withdraw_amount: u64, deposit_index: usize) -> ProgramResult { - let collateral = &mut self.deposits[deposit_index]; + 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(deposit_index); + self.deposits.remove(collateral_index); } else { collateral.withdraw(withdraw_amount)?; } @@ -71,10 +71,10 @@ impl Obligation { } /// Repay liquidity and remove it from borrows if zeroed out - pub fn repay(&mut self, settle_amount: Decimal, borrow_index: usize) -> ProgramResult { - let liquidity = &mut self.borrows[borrow_index]; + 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(borrow_index); + self.borrows.remove(liquidity_index); } else { liquidity.repay(settle_amount)?; } @@ -139,26 +139,26 @@ impl Obligation { } /// Find collateral by deposit reserve - pub fn find_deposit( + pub fn find_collateral_in_deposits( &self, deposit_reserve: Pubkey, ) -> Result<(&ObligationCollateral, usize), ProgramError> { if self.deposits.is_empty() { return Err(LendingError::ObligationCollateralEmpty.into()); } - let deposit_index = self - ._find_deposit_index(deposit_reserve) + let collateral_index = self + ._find_collateral_index_in_deposits(deposit_reserve) .ok_or(LendingError::InvalidObligationCollateral)?; - Ok((&self.deposits[deposit_index], deposit_index)) + Ok((&self.deposits[collateral_index], collateral_index)) } /// Find or add collateral by deposit reserve - pub fn find_or_add_deposit( + pub fn find_or_add_collateral_to_deposits( &mut self, deposit_reserve: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { - if let Some(deposit_index) = self._find_deposit_index(deposit_reserve) { - Ok(&mut self.deposits[deposit_index]) + if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) { + Ok(&mut self.deposits[collateral_index]) } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { @@ -168,33 +168,33 @@ impl Obligation { } } - fn _find_deposit_index(&self, deposit_reserve: Pubkey) -> Option { + 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_borrow( + pub fn find_liquidity_in_borrows( &self, borrow_reserve: Pubkey, ) -> Result<(&ObligationLiquidity, usize), ProgramError> { if self.borrows.is_empty() { return Err(LendingError::ObligationLiquidityEmpty.into()); } - let borrow_index = self - ._find_borrow_index(borrow_reserve) + let liquidity_index = self + ._find_liquidity_index_in_borrows(borrow_reserve) .ok_or(LendingError::InvalidObligationLiquidity)?; - Ok((&self.borrows[borrow_index], borrow_index)) + Ok((&self.borrows[liquidity_index], liquidity_index)) } /// Find or add liquidity by borrow reserve - pub fn find_or_add_borrow( + pub fn find_or_add_liquidity_to_borrows( &mut self, borrow_reserve: Pubkey, ) -> Result<&mut ObligationLiquidity, ProgramError> { - if let Some(borrow_index) = self._find_borrow_index(borrow_reserve) { - Ok(&mut self.borrows[borrow_index]) + if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) { + Ok(&mut self.borrows[liquidity_index]) } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { @@ -204,7 +204,7 @@ impl Obligation { } } - fn _find_borrow_index(&self, borrow_reserve: Pubkey) -> Option { + fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option { self.borrows .iter() .position(|liquidity| liquidity.borrow_reserve == borrow_reserve) From 479dc2650186f59dabca860b750cac72dcf7831b Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 26 Mar 2021 18:27:38 -0500 Subject: [PATCH 119/191] [WIP] move loan_to_value_ratio and liquidation_threshold back to reserve --- token-lending/program/src/instruction.rs | 46 ++++------ token-lending/program/src/processor.rs | 59 ++++++------- .../program/src/state/lending_market.rs | 60 ++----------- token-lending/program/src/state/obligation.rs | 2 + token-lending/program/src/state/reserve.rs | 86 ++++++++++--------- 5 files changed, 95 insertions(+), 158 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 021f94e2e64..33b4adea987 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -27,10 +27,6 @@ pub enum LendingInstruction { InitLendingMarket { /// Owner authority which can add new reserves owner: Pubkey, - /// The target ratio of an obligation's borrows to deposits as a percent - loan_to_value_ratio: u8, - /// The percent at which an obligation is considered unhealthy - liquidation_threshold: u8, }, // 1 @@ -283,14 +279,8 @@ impl LendingInstruction { .ok_or(LendingError::InstructionUnpackError)?; Ok(match tag { 0 => { - let (owner, rest) = Self::unpack_pubkey(rest)?; - let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; - let (liquidation_threshold, _rest) = Self::unpack_u8(rest)?; - Self::InitLendingMarket { - owner, - loan_to_value_ratio, - liquidation_threshold, - } + let (owner, _rest) = Self::unpack_pubkey(rest)?; + Self::InitLendingMarket { owner } } 1 => { let (new_owner, _rest) = Self::unpack_pubkey(rest)?; @@ -299,22 +289,24 @@ impl LendingInstruction { 2 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; - let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; - let (collateral_enabled, rest) = Self::unpack_bool(rest)?; let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; + let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; + let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; + let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?; let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { optimal_utilization_rate, - liquidation_bonus, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, - collateral_enabled, + loan_to_value_ratio, + liquidation_threshold, + liquidation_bonus, fees: ReserveFees { borrow_fee_wad, host_fee_percentage, @@ -418,15 +410,9 @@ impl LendingInstruction { pub fn pack(&self) -> Vec { let mut buf = Vec::with_capacity(size_of::()); match *self { - Self::InitLendingMarket { - owner, - loan_to_value_ratio, - liquidation_threshold, - } => { + Self::InitLendingMarket { owner } => { buf.push(0); buf.extend_from_slice(owner.as_ref()); - buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); - buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); } Self::SetLendingMarketOwner { new_owner } => { buf.push(1); @@ -437,11 +423,12 @@ impl LendingInstruction { config: ReserveConfig { optimal_utilization_rate, - liquidation_bonus, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, - collateral_enabled, + loan_to_value_ratio, + liquidation_threshold, + liquidation_bonus, fees: ReserveFees { borrow_fee_wad, @@ -452,11 +439,12 @@ impl LendingInstruction { buf.push(2); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&optimal_utilization_rate.to_le_bytes()); - buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); buf.extend_from_slice(&min_borrow_rate.to_le_bytes()); buf.extend_from_slice(&optimal_borrow_rate.to_le_bytes()); buf.extend_from_slice(&max_borrow_rate.to_le_bytes()); - buf.extend_from_slice(&u8::from(collateral_enabled).to_le_bytes()); + buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); + buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); + buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } @@ -505,8 +493,6 @@ impl LendingInstruction { /// Creates an 'InitLendingMarket' instruction. pub fn init_lending_market( program_id: Pubkey, - loan_to_value_ratio: u8, - liquidation_threshold: u8, lending_market_pubkey: Pubkey, lending_market_owner: Pubkey, quote_token_mint: Pubkey, @@ -521,8 +507,6 @@ pub fn init_lending_market( ], data: LendingInstruction::InitLendingMarket { owner: lending_market_owner, - loan_to_value_ratio, - liquidation_threshold, } .pack(), } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 636edef6b5a..ff9c034ea0f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -35,19 +35,9 @@ pub fn process_instruction( ) -> ProgramResult { let instruction = LendingInstruction::unpack(input)?; match instruction { - LendingInstruction::InitLendingMarket { - owner, - loan_to_value_ratio, - liquidation_threshold, - } => { + LendingInstruction::InitLendingMarket { owner } => { msg!("Instruction: Init Lending Market"); - process_init_lending_market( - program_id, - owner, - loan_to_value_ratio, - liquidation_threshold, - accounts, - ) + process_init_lending_market(program_id, owner, accounts) } LendingInstruction::SetLendingMarketOwner { new_owner } => { msg!("Instruction: Set Lending Market Owner"); @@ -106,19 +96,8 @@ pub fn process_instruction( fn process_init_lending_market( program_id: &Pubkey, owner: Pubkey, - loan_to_value_ratio: u8, - liquidation_threshold: u8, accounts: &[AccountInfo], ) -> ProgramResult { - if loan_to_value_ratio >= 100 { - msg!("Loan to value ratio must be in range [0, 100)"); - return Err(LendingError::InvalidConfig.into()); - } - if liquidation_threshold <= loan_to_value_ratio || liquidation_threshold > 100 { - msg!("Liquidation threshold must be in range (LTV, 100]"); - return Err(LendingError::InvalidConfig.into()); - } - let account_info_iter = &mut accounts.iter(); let lending_market_info = next_account_info(account_info_iter)?; let quote_token_mint_info = next_account_info(account_info_iter)?; @@ -138,8 +117,6 @@ fn process_init_lending_market( owner, quote_token_mint: *quote_token_mint_info.key, token_program_id: *token_program_id.key, - loan_to_value_ratio, - liquidation_threshold, }); LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; @@ -187,10 +164,6 @@ fn process_init_reserve( msg!("Optimal utilization rate 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.optimal_borrow_rate < config.min_borrow_rate { msg!("Optimal borrow rate must be >= min borrow rate"); return Err(LendingError::InvalidConfig.into()); @@ -199,6 +172,20 @@ fn process_init_reserve( msg!("Optimal borrow rate must be <= max borrow rate"); 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_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.liquidation_bonus > 100 { + msg!("Liquidation bonus must be in range [0, 100]"); + 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()); @@ -742,7 +729,7 @@ fn process_deposit_obligation_collateral( if deposit_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } - if !deposit_reserve.config.collateral_enabled { + if deposit_reserve.config.loan_to_value_ratio == 0 { return Err(LendingError::ReserveCollateralDisabled.into()); } @@ -899,7 +886,9 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); + // @FIXME: LTV + let loan_to_value_ratio = Rate::from_percent(50); + // let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; if loan_to_value >= loan_to_value_ratio { return Err(LendingError::ObligationLoanToValueLimit.into()); @@ -1041,7 +1030,9 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); + // @FIXME: LTV + let loan_to_value_ratio = Rate::from_percent(50); + // let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); let loan_to_value = obligation.loan_to_value()?; if loan_to_value >= loan_to_value_ratio { return Err(LendingError::ObligationLoanToValueLimit.into()); @@ -1327,7 +1318,9 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); + // @FIXME: LTV + let liquidation_threshold = Rate::from_percent(55); + // let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); let loan_to_value = obligation.loan_to_value()?; if loan_to_value < liquidation_threshold { return Err(LendingError::ObligationHealthy.into()); diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 793610e297a..7ccdf956ba5 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -17,10 +17,6 @@ pub struct LendingMarket { pub token_program_id: Pubkey, /// Quote currency token mint pub quote_token_mint: Pubkey, - /// The target ratio of an obligation's borrows to deposits as a percent - pub loan_to_value_ratio: u8, - /// The percent at which an obligation is considered unhealthy - pub liquidation_threshold: u8, /// Owner authority which can add new reserves pub owner: Pubkey, } @@ -33,10 +29,6 @@ pub struct InitLendingMarketParams { pub token_program_id: Pubkey, /// Quote currency token mint pub quote_token_mint: Pubkey, - /// The target ratio of an obligation's borrows to deposits as a percent - pub loan_to_value_ratio: u8, - /// The percent at which an obligation is considered unhealthy - pub liquidation_threshold: u8, /// Owner authority which can add new reserves pub owner: Pubkey, } @@ -56,8 +48,6 @@ impl LendingMarket { self.owner = params.owner; self.quote_token_mint = params.quote_token_mint; self.token_program_id = params.token_program_id; - self.loan_to_value_ratio = params.loan_to_value_ratio; - self.liquidation_threshold = params.liquidation_threshold; } } @@ -69,39 +59,19 @@ impl IsInitialized for LendingMarket { } // @TODO: adjust padding. what's a reasonable number? -const LENDING_MARKET_LEN: usize = 160; // 1 + 1 + 32 + 32 + 1 + 1 + 32 + 60 +const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128 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, - token_program_id, - quote_token_mint, - loan_to_value_ratio, - liquidation_threshold, - owner, - _padding, - ) = mut_array_refs![ - output, - 1, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 1, - 1, - PUBKEY_BYTES, - 60 - ]; + let (version, bump_seed, token_program_id, quote_token_mint, owner, _padding) = + mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; *version = self.version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); token_program_id.copy_from_slice(self.token_program_id.as_ref()); quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); - *loan_to_value_ratio = self.loan_to_value_ratio.to_le_bytes(); - *liquidation_threshold = self.liquidation_threshold.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); } @@ -109,26 +79,8 @@ impl Pack for LendingMarket { 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, - quote_token_mint, - token_program_id, - loan_to_value_ratio, - liquidation_threshold, - owner, - _padding, - ) = array_refs![ - input, - 1, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 1, - 1, - PUBKEY_BYTES, - 60 - ]; + let (version, bump_seed, quote_token_mint, token_program_id, owner, _padding) = + array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { return Err(ProgramError::InvalidAccountData); @@ -139,8 +91,6 @@ impl Pack for LendingMarket { bump_seed: u8::from_le_bytes(*bump_seed), token_program_id: Pubkey::new_from_array(*token_program_id), quote_token_mint: Pubkey::new_from_array(*quote_token_mint), - loan_to_value_ratio: u8::from_le_bytes(*loan_to_value_ratio), - liquidation_threshold: u8::from_le_bytes(*liquidation_threshold), owner: Pubkey::new_from_array(*owner), }) } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index eb43362d8d6..bfaf7980497 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -112,12 +112,14 @@ impl Obligation { Rate::try_from(self.borrowed_value()?.try_div(deposited_value)?) } + // @FIXME: LTV /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio pub fn max_withdraw_value(&self, loan_to_value_ratio: Rate) -> Result { let min_deposited_value = self.borrowed_value()?.try_div(loan_to_value_ratio)?; self.deposited_value()?.try_sub(min_deposited_value) } + // @FIXME: LTV /// Calculate the maximum liquidity value that can be borrowed for a given loan to value ratio pub fn max_borrow_value(&self, loan_to_value_ratio: Rate) -> Result { self.deposited_value()? diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b090f21b5dd..d6c2fff1806 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -544,16 +544,18 @@ impl From for Rate { pub struct ReserveConfig { /// Optimal utilization rate as a percent pub optimal_utilization_rate: u8, - /// The percent bonus the liquidator gets when repaying liquidity to an unhealthy obligation - pub liquidation_bonus: 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, - /// Collateral is enabled for borrows - pub collateral_enabled: bool, + /// Ratio of the value of borrows to deposits as a percent; 0 if use as collateral is disabled + pub loan_to_value_ratio: u8, + /// The percent at which an obligation is considered unhealthy + pub liquidation_threshold: u8, + /// The percent bonus the liquidator gets when repaying liquidity to an unhealthy obligation + pub liquidation_bonus: u8, /// Program owner fees assessed, separate from gains due to interest accrual pub fees: ReserveFees, } @@ -637,7 +639,7 @@ impl IsInitialized for Reserve { } // @TODO: adjust padding. what's a reasonable number? -const RESERVE_LEN: usize = 610; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 300 +const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -661,14 +663,15 @@ impl Pack for Reserve { collateral_supply, collateral_mint, collateral_mint_supply, - optimal_utilization_rate, - liquidation_bonus, - min_borrow_rate, - optimal_borrow_rate, - max_borrow_rate, - collateral_enabled, - borrow_fee_wad, - host_fee_percentage, + config_optimal_utilization_rate, + config_min_borrow_rate, + config_optimal_borrow_rate, + config_max_borrow_rate, + config_loan_to_value_ratio, + config_liquidation_threshold, + config_liquidation_bonus, + config_fees_borrow_fee_wad, + config_fees_host_fee_percentage, _padding, ) = mut_array_refs![ output, @@ -694,9 +697,10 @@ impl Pack for Reserve { 1, 1, 1, + 1, 8, 1, - 300 + 256 ]; *version = self.version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); @@ -726,14 +730,15 @@ impl Pack for Reserve { *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); // config - *optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); - *liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); - *min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); - *optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); - *max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); - pack_bool(self.config.collateral_enabled, collateral_enabled); - *borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); - *host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); + *config_optimal_utilization_rate = self.config.optimal_utilization_rate.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_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes(); + *config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes(); + *config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); + *config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); + *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); } /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). @@ -757,14 +762,15 @@ impl Pack for Reserve { collateral_supply, collateral_mint, collateral_mint_supply, - optimal_utilization_rate, - liquidation_bonus, - min_borrow_rate, - optimal_borrow_rate, - max_borrow_rate, - collateral_enabled, - borrow_fee_wad, - host_fee_percentage, + config_optimal_utilization_rate, + config_min_borrow_rate, + config_optimal_borrow_rate, + config_max_borrow_rate, + config_loan_to_value_ratio, + config_liquidation_threshold, + config_liquidation_bonus, + config_fees_borrow_fee_wad, + config_fees_host_fee_percentage, __padding, ) = array_refs![ input, @@ -790,9 +796,10 @@ impl Pack for Reserve { 1, 1, 1, + 1, 8, 1, - 300 + 256 ]; Ok(Self { version: u8::from_le_bytes(*version), @@ -818,15 +825,16 @@ impl Pack for Reserve { supply_pubkey: Pubkey::new_from_array(*collateral_supply), }, config: ReserveConfig { - optimal_utilization_rate: u8::from_le_bytes(*optimal_utilization_rate), - liquidation_bonus: u8::from_le_bytes(*liquidation_bonus), - min_borrow_rate: u8::from_le_bytes(*min_borrow_rate), - optimal_borrow_rate: u8::from_le_bytes(*optimal_borrow_rate), - max_borrow_rate: u8::from_le_bytes(*max_borrow_rate), - collateral_enabled: unpack_bool(collateral_enabled)?, + optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate), + 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: u8::from_le_bytes(*config_max_borrow_rate), + loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), + liquidation_threshold: u8::from_le_bytes(*config_liquidation_threshold), + liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus), fees: ReserveFees { - borrow_fee_wad: u64::from_le_bytes(*borrow_fee_wad), - host_fee_percentage: u8::from_le_bytes(*host_fee_percentage), + borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), + host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), }, }, }) From c6443e8925f18b5f917aced9ea7ac70e14b00b07 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 26 Mar 2021 18:27:51 -0500 Subject: [PATCH 120/191] remove unused memory account --- token-lending/program/src/processor.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ff9c034ea0f..2e63f23f020 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -965,14 +965,9 @@ fn process_borrow_obligation_liquidity( let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - if memory.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); From 6c0548d3ca203ae185eb84a4b98a5e08e8fc52b4 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 26 Mar 2021 18:28:42 -0500 Subject: [PATCH 121/191] remove serum dex --- Cargo.lock | 81 ------------------ token-lending/program/Cargo.toml | 1 - .../program/tests/fixtures/README.md | 13 --- .../tests/fixtures/sol_usdc_dex_market.bin | Bin 388 -> 0 bytes .../fixtures/sol_usdc_dex_market_asks.bin | Bin 65548 -> 0 bytes .../fixtures/sol_usdc_dex_market_bids.bin | Bin 65548 -> 0 bytes .../tests/fixtures/srm_usdc_dex_market.bin | Bin 388 -> 0 bytes .../fixtures/srm_usdc_dex_market_asks.bin | Bin 65548 -> 0 bytes .../fixtures/srm_usdc_dex_market_bids.bin | Bin 65548 -> 0 bytes 9 files changed, 95 deletions(-) delete mode 100644 token-lending/program/tests/fixtures/README.md delete mode 100644 token-lending/program/tests/fixtures/sol_usdc_dex_market.bin delete mode 100644 token-lending/program/tests/fixtures/sol_usdc_dex_market_asks.bin delete mode 100644 token-lending/program/tests/fixtures/sol_usdc_dex_market_bids.bin delete mode 100644 token-lending/program/tests/fixtures/srm_usdc_dex_market.bin delete mode 100644 token-lending/program/tests/fixtures/srm_usdc_dex_market_asks.bin delete mode 100644 token-lending/program/tests/fixtures/srm_usdc_dex_market_bids.bin diff --git a/Cargo.lock b/Cargo.lock index 1d5daae5ef8..bc6cf3c5972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,12 +40,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-traits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483" - [[package]] name = "ansi_term" version = "0.11.0" @@ -366,12 +360,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a4bad0c5981acc24bc09e532f35160f952e35422603f0563cd7a73c2c2e65a0" - [[package]] name = "byteorder" version = "1.4.2" @@ -942,26 +930,6 @@ dependencies = [ "syn 1.0.58", ] -[[package]] -name = "enumflags2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" -dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.8", - "syn 1.0.58", -] - [[package]] name = "env_logger" version = "0.8.2" @@ -1009,16 +977,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" -[[package]] -name = "field-offset" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40e7a744c1d22cd64783732a287dd5d08a9f0e1d89b685bf084aab753cb20d4" -dependencies = [ - "memoffset 0.5.6", - "rustc_version", -] - [[package]] name = "filetime" version = "0.2.13" @@ -2746,12 +2704,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "safe-transmute" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b8b2cd387f744f69469aaed197954ba4c0ecdb31e02edf99b023e0df11178a" - [[package]] name = "same-file" version = "1.0.6" @@ -2927,29 +2879,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "serum_dex" -version = "0.2.0" -source = "git+https://github.com/project-serum/serum-dex?rev=991a86e#991a86e93c22667b6b0fbb01914395d332be5531" -dependencies = [ - "arrayref", - "bincode", - "bytemuck", - "byteorder", - "enumflags2", - "field-offset", - "itertools", - "num-traits", - "num_enum", - "safe-transmute", - "serde", - "solana-program", - "spl-token 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "static_assertions", - "thiserror", - "without-alloc", -] - [[package]] name = "sha-1" version = "0.8.2" @@ -3938,7 +3867,6 @@ dependencies = [ "proptest", "serde", "serde_yaml", - "serum_dex", "solana-program", "solana-program-test", "solana-sdk", @@ -4923,15 +4851,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "without-alloc" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e34736feff52a0b3e5680927e947a4d8fac1f0b80dc8120b080dd8de24d75e2" -dependencies = [ - "alloc-traits", -] - [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index 6ccc2a30499..0b26cee5ccd 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -17,7 +17,6 @@ arrayref = "0.3.6" flux-aggregator = { git = "https://github.com/octopus-network/solana-flux-aggregator", rev = "9cfaec5", features = ["no-entrypoint"] } num-derive = "0.3" num-traits = "0.2" -serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "991a86e", features = ["no-entrypoint"] } solana-program = "1.5.11" spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] } thiserror = "1.0" diff --git a/token-lending/program/tests/fixtures/README.md b/token-lending/program/tests/fixtures/README.md deleted file mode 100644 index e34f5763470..00000000000 --- a/token-lending/program/tests/fixtures/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# fixtures - -### SOL / USDC / SRM Dex Accounts - -``` -$ solana config set --url https://api.mainnet-beta.solana.com -$ solana account 9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT --output-file sol_usdc_dex_market.bin -$ solana account 14ivtgssEBoBjuZJtSAPKYgpUK7DmnSwuPMqJoVTSgKJ --output-file sol_usdc_dex_market_bids.bin -$ solana account CEQdAFKdycHugujQg9k2wbmxjcpdYZyVLfV9WerTnafJ --output-file sol_usdc_dex_market_asks.bin -$ solana account ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA --output-file srm_usdc_dex_market.bin -$ solana account AuL9JzRJ55MdqzubK4EutJgAumtkuFcRVuPUvTX39pN8 --output-file srm_usdc_dex_market_bids.bin -$ solana account 8Lx9U9wdE3afdqih1mCAXy3unJDfzSaXFqAvoLMjhwoD --output-file srm_usdc_dex_market_asks.bin -``` \ No newline at end of file diff --git a/token-lending/program/tests/fixtures/sol_usdc_dex_market.bin b/token-lending/program/tests/fixtures/sol_usdc_dex_market.bin deleted file mode 100644 index 31a967fe2060c5b2e7122e43b931e5c62b7ed218..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 388 zcmXR;Eh^1rW`KZ}L%$`~U6cIkmaG>1o{ecf(~6Y+7Mg8^jk^`@%NE8MFhbR_&F%>Q zx4NDoBBCx^~n;$jk}*5y7cYA zlbU5r%&z*eiSlY+Ke)*_2<(kHajGEg?$VZJ?+PnTj+L+dawvYUXqGO+rAmYKPEgB$ zRMz9SMpgVBp394-$WCYFx613;ao1|kZIObGnx?J3_GUJ!9a#}Tbz<^=v_QI2Km-u< z{yFirn*UsNXt!>u=YU%kE!{ z2`Fd%X*_$Xjg;@-J#!AW-PKRJ@3At1B~Gg>D3amfS5}ZaVD162L7>F>6gVgg5>rw# G^U?uu37b~{ diff --git a/token-lending/program/tests/fixtures/sol_usdc_dex_market_asks.bin b/token-lending/program/tests/fixtures/sol_usdc_dex_market_asks.bin deleted file mode 100644 index c8c8b6564d8b0e46cb918f5506f00c813acfea41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65548 zcmeI2dvwob9LIlVk}+A!noEq9vXxK>Q*Mp6ntLLawMnk6nUo5VOu3DkFQK0^J$c=hpRaeDqR$)jIF#Y@5vN@uxpkbS~xzAl+ z++y(CNAkvx%GmjGS&c@+M~3y-HvY8k6#HGo{NkVOm055c5kh`-so*?VU1JDw^yVfpSH#JFwMvu4kBHS2xoNdT_03|gpDH;; z%Fk^g)?LiD6Z+NOHrXK$oJfau+a{ccmkZdbWZAFup0ct-&i?ZEc5fUIonL#&_6@%+ ziHNBfyd|t>@xbSQ?~>oNLwHGD<)QS<2QSRLu&VNW7NwDiGX9|AloW2bXZQ4h?T`0r z*RNF|c}BCOvV;+nW2b#z6u15K?(-{~X$Ojay{X0A?1F>)q1OqVE4)HPJvhleaA3Ey zm`|wd^rMavnj%)}RwM(4B{;W;xm2Cc!_@hlC}P(eYO6M*U$IpKF1NvX!DVgLGH@DK z4Y=F}C&y)N)w1AqQPN#r7kzQF7E{Ohhl;vAkyz`@=c9hvdFXJ~{*vZfBG#P$woO#{ zg^j1*kCNFk7t0{0lj_{6uClG(EffEUOS$#7#UI400=$C%YRcW@~1gDO5cJ+ys{Suvx&6PQuy2cO)IUIDkI z40&3DbG0XeECwguCr%@uI4}9cxyC2X1fMw9`ou}_iF3aX99#`}USzBH!#e6df2oMo z4uK!!+@YQqvlVBBV5|&*gY&sf2>I2ef)id{V+e6@T7-~aT`D*?R@WFp9Gw0kOh$ zU+Ul*idaktWN>gkPZ%wWb|%U7Jh6IKZdxtj`E{#I=Ol1?IHy>(g45P2(>V#8Ud}02 zt>C0uWjZH;qb@1uL_;e$eGNG_&ET}MNi^i(Jj8XHkcO>5Ab39Gw0xj4Vzbgj4Hz4ZQ?}}&CoV8=nC+}}=(j;MOgW-vrQ<4wgzAO6Z-XV`^c=YQ|E#_ty zoKbEHwHk0R9_4ic=Ps`hQ4bEr0lZG&^z#Z4_4wYe60oi6e%Mt#6iv^aGwOHTPapK( zyn0KMW#B{$&P>ILv*Z-cmYrBIw(YmePyX`<`ZZXFJT1X_&J#fvgLAh}oID>mxXSSy z{9AdRqdbogv3o<{EhnvpBAw%Vdly&@WcFL&KBj{H%!8xz_F>yA0EIF^9Don~hmh&|@jc`1Q6a4*U z;yjcuInjHD+*H5BtP<-Y3pvA2@jCU_45HevK5d^bq(#&W zHr?mP4U^wX;>fu@=Vk(F5y0kKZ#NoEX7*SG_-{s^0}B z&J1<3?B7)PhxF0KmG2vA=EF8-z|S0GmN@~OXUu8F3~=Tev&;#&PN87iSX@ zYich4F9RIBT6S0Wjdb<*+~y({CuZ9T{hDu^?2rdeABT3^CUDfNux-gj^T1hf(U#DH z;HaVE&DM98gQP{Vs3W9 zxxr1LRs&9v7IU);jy?wLmh+;ULaheJldHwt?1J;Mn?kJyoL9A&n_X~jbyKL-fU{hS zx!DEhayNxq4W2uAWa;COLq+T!E!Sxm?;DtwssESwRK0KHT(@yR<_|v~jVp-w@Wrr2 zdrx#dA75Vn)tTvS^A3j9ULCnUQq~2R=+_&;9S>`8)DXD`ia-BzfdjzttnzduvKX8- zK5CDYo*Iomcp&emf)JvP;2H92;deeaQfuVYJ0cN*5pVU6>r(I+^)W#9gZ!>P=0 z%97;J(D~Eo6P*6CpW5wkvXs+HV)q#loTM6Zs2a0HnJX0>>|wX+0;nmVq&?2KPV2Zb zickE$>*&6rjVcmW9jd&vDzWL+;RmZ{emZQ; z#Wz)e)Ay!--|C-OM>P}JCS6C(zuO@Nt;eqD_sYJbgG%3;@lP>o1?O?2O#dWs^i=yN2DF0nQb3ML zGdTB{BnIT*dafahl%I6{lc|&Ah~rZ}&(FQS@O;m?$9D8xxbw_`?K87m{hj(%O4fp_ zTRZn{^MnJ=y|(h_&Yg;yf>U2Qbx}EEloNDcT<_{|ih3OV;kSf84`N(BoKUDwaGHc7 zIK|+UM8wf4AQTOrtI7F_ea;O!UrBmCvQ~}G>#66fjts6$=dn!fT+sQ-dD!WGzc0>C z{4sIo8Ti5aIqU{bd{~IogQJ&JC>l6e&xhT>c{D6U>cQz35$C~(IEy3T;Cikp*K_Ri zSJ3rbHUG@3ap6=my`Jldq^){AR~TN;hxH_Q>qfGxK zaN7H)7`1})j!~w65;)oZDMqc}=vTE-zE=`BgS{Fp;@}Li$oEPG=Q*!Ni#RyLEb_fl z!5Qn-Xb}hJHH&<&RB)d3YP5)hGtwg8D-|64UJ!P}yn!h@Ugw{_I&aL8SeXSJ9DJV+ zR!-IWXpqDPT5vF~(H8k$so<>hYP5)BTv(5t(Rp>U`q?0{jkmy=7vuRUVT`iiU|i!Y z^1V{Q+3MA35r-cv4c6;AAweIWJ4vjgB?q3%l`~FXnoN~g(JgSk)$_GP&sT*oR#|fJ zJKAPFUn7;1E3q*a9C+4O5f)YMRB-a$Ve1Gu3$3f%1>j6@hpi*vOt!9a7l1S09kz~u zGt;`tT>ws@J8T`n`mlk(_UnC7s`o|EbLDmY&o@mSzTwj1q(k5&3Mcmd3gVo5+39lb zfSk{|l_pkTT+^LUs84X(gd#Y_;LM1KgZn+~hW-?Xg-AU(4@JazJ|a$?h&ZJYaYjVM zsTUFFg@`x_5piCOfP;A>NiN1e^!v2Geh&uC8$0x&zie}Qhlb_ZH{Q=o_&IIV>fJO2dDV_o#(O)s4?Y0mu1=P<4nMm_gW0%xdyicu@Z z)m&h)&)q@eDo=6njYgmR==Aa32f?W>?~P;MADX^5KJrgW+CY78Tub^B`}`HupT@PE z{(QG*+0vrf8<(L!@zS3Y`hD@9p7)^s?62)`iZkc7%+vm)N`GRXPlNii1`m3&ovxSD zyY2`17to(-!ioKTIf(PIzRZ}=_Lsusu5rKOc^N}Fr0e-D`d)pr#NL+ObOQ&!>&5wHrnHZTP0;nzKwnyv z&F!9w-(fMXx4;Uyfio*41Lg43OZF>zU#!*pV!XuiWH;TaINS6(%+u@e#&vEAkvxnm z-6G#B6`Uc=LY@(w&KeQ(?BZgY(v*{fzXI=q_i{ic3f+dCCs)wj2W3KP6<6bQg9 z1JYb^ogD%ONj?V;=^L5HMQIrGEGubFno_;>%Z9q;r*B>2EV=wl6R2*Ek}p_W3RLf} zfWPnUA0MrXC*ah)BcqW zj!SMnyCQ74K{Yq^vTnv?MfUlRGb9=u(~HX{b_e`UIDJTSHE)b$sya>GQ|Ka+hsVnhvaudwKe=%e|yqM+_FUF@jwJa}U&63}?PgWdgAa5>rw# G^U?v~{-Wpr diff --git a/token-lending/program/tests/fixtures/srm_usdc_dex_market_asks.bin b/token-lending/program/tests/fixtures/srm_usdc_dex_market_asks.bin deleted file mode 100644 index 45e834e6d0a7d72023a0485756d6f9b31c40e1a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65548 zcmeI3d$?6|8pn?~6{iuZsi>qd?vs%dDkiy`n8;=5WQ>}arZb61l1dbc3MCnm?lU?v zxpYxF(M(Ju8Z<^noGwUBjX}|j`F;2Ax1Rkx>v?+Dd-m`3-*-LFy1w7{v-kS!wbx#2 zpM7rX)qgT10y^FfKwM%kabd$YZ ztd=e+rog#cOz4W(uXb)6`blxFVdbH|?}u*I$SbS?7u+*st2TEpZe7 zD#bC-jps8s9+TtLG2-lJ#JSFhQ_qNVj}d2_5$6X+oCAzFBMmrspYG)j>X94$`}D%b z9-8$&-55>=Ysd5|t6OX4l8OG3!j5i!%23&+xDj#@RQelP;lbNqwr%SxZJ(`xpN?)! zU$-3Swk7vyZZUJfeqETE$_{{YVsw!GB;3aTQF1S122oBy~u>&}LV#M9gH~P zBO;21=ksC|!8e10_j~LB&QD_@t{ZNxdph_l3qbDj~Wn-S+J zBhL9ooSzwSHW_h#WyI-X#My4dx!8b%>y4kdv1^sTFQ{tep+nv5PQR#{;(DXV!P4J{ zPbmLl!8Nlku53GgYx|BP7hhNM&BGJETRLY{>yOIo&1ih?H2*;n zZH26Y^HCvzdGA(o8w09fcM34ZCJ>GGfAjxOW=&q zhJ_qBJ%qZp1kTagu#f}iZlSI%fip@Q7INVHO{i;2;2f+C3psG^73$g&IHR>;AqUQ% zg}Sx`&Q02|kOOCoP}i2gS*r~TIdC2n>e>=GGGg+u>OW>TX4GTGN zelOIuC2*!_!$J<6&O%*V0%xi=EabqMC)Bkia7wjdAqURWLS0(|r@l5U(hJ_qBB|=?W0_P%a zSjd6%vQXESzk<9TT~p0hZY?BhWvPhPU>o{8OdVZRy#cFYW%*J1#n7o22YV{qUc76S;q z;H)v?tTf{M)rd3Ah;zIVXO$5rTv|ra@VdVlMexnw{Kts%mJ#RgMjU*e#t!iFWh}(? zgY%9NXOI!+Jp&G|TT0!S{=YVy#<9=Z0i1SjT)xoX7qn{PJCAg;BmAOjiqD;5_rU4@ zm&y9vx%^Cz|90MhE^AKz?>KO9Q+WM6|9sAA>g$UatnM$zwD-m;Wsw4W7R&XXHD-m;WHb(R(t>Ao;tVGPg`CUYR(hAOJ$x6f=oUbGL zlUAHJ8ar6}|G%=%KkJ%%!g-HXK6vT4+qU3*c$yp2-)~g7ZO{6sYKr}OUX_$X;B1v6 zswy})s*-XDoPWs?RTZ3Gs-zqOXNMe7Rl(__O3EQ{zK|oTDmY)Ml5z+f{LVaf0B2_` z#Px%-&45D$4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp m4bT7$&;Sk401eOp4bT7$&;Sk401bTK2Krswv*!)vz5fq)y<%Aa diff --git a/token-lending/program/tests/fixtures/srm_usdc_dex_market_bids.bin b/token-lending/program/tests/fixtures/srm_usdc_dex_market_bids.bin deleted file mode 100644 index f72182902ccd98eefb94f236ab1fff81418bc702..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65548 zcmeI2X>b%p7=|GUNeB=wIV2$@97balg+x#h6fhzJ9w2gf5G4XeJOV0sfC@t764Zbk z5~Co>Eg%G<<%MEdU_q&E0?GcXqmW`sdqKGqc3U800#n}x=V0w;0~ zF#`h%!MH%Syf!E6+VyN}RiRW4x2umvdvMN|t^TQsQ>;1XNnW85K?R{*r&`4Og@V(- zuh1eNoK%Z=zff@CCu{;IEi6RpgLAC|PJ%FU1XiQ3drd0^t*JTjSxQ-_uUBX9tGMKY zZRyX=OUVDI*OAgkw!OUYz;90$Owo-vnDa*I$WHr?gVS6%=XFv2XQv^jSrhg5HcmWI zIJ36Z6KGeu$_ljy=k8DhRT=F{POew4iymL=^tk9y(e%wfC>e z-#qKb?(Ie%KQMk#W&84$s{W#Bxv9mcm$n8cQBJD8=B(G8AkRbeB=O4bbJkCPEAKek z)k2{|?ZLSy6hT$S>0ypo*GJDAZB%}HIb^AiM*9e7O^KSHXX-}ImfGb;1QmpKX|)!$ z{vQg?kpE{}=is{6m;U3OpM&~Ovt0G34&FQeu^V;&NfOR!y8l!d_QNA*E6#vpX-Q** zgLXxJ6IDrkdvH#Ruj+qlfs^^4ocO+gqoVxf~Dkj zx%#)ke{2OORXB&U)pZ|d$QjsGai-V)y|ZlZ5BuS{K%GLUJve!x2&!`AdVQ*|*HGOL z^QHJq3qJg$$Vp}Eagkv-E|%-(f_~TaDj9g7(QLE}zk^{DIDNxHq&_(4&tVfdH-&{r zeQ+>ugiYYA3=5I^;9#7FP2h|R3z7QhhY8ZD*ZioS3*Kw4pw0ihCb4Ww~sugqP4q8#m- zs~_RC-BKZ5`cnZW53l7Z^rJmZKn*b(O{M;z~* z6b}}Du8F6jrw%x`dh+7=iu(<$i+cO>bCOpnn`w%0yz7VO2%xJeMB>n{;TG|Jq2S!< zS7?zB&IpTmzff?J{R%De!5L){?-z=73>OLKbG-u)IC{w}6b+mkLlIPEa7H@fjB&)7 z;E406BTlg+&R9pB(T+G3jyNTbIO8307CGYF=7>}6fP>#1Jb!uD)!Ry*ewAVLSHQu# zR)&7=-ldwBHk|)oSXd$E%L!BvxxT#1?Nh? zLW_KGCR@b&g@S{7`mia&@cy5}j?%u_DO)F3e{%BYj>9&U^jNTcSd$J+0K;{>}GM2;9z~MC||9!Eo`bP-y?^6)kmW}IEAu}X`|KwUe|*Y zuV?fljS&Z)?>CB!9R$t`u|teX!NL2cunC+8!a}4zIMW<)HaOxu?1V|>l=Vzh#+zf3Ziek8PK{ zS4~ew;^4WTMZ8}qI8XQ$TI7TCq(!`6C^)zH6j{h-=EIJ4V0vy@&0+1imR;JTa~qRz;bXBWt{fa z&pQ>GV>(V>8?~`+oT6O~6*|-&oaLbisxmlh9dVv@z=7v%IjPC|dKKw;Ea-a8)$a@1 z{<`G(;zm0cVLrrrPw)GJfs%K*Y^Di*o;MAS5eLq~80Dr#;H)wYju8ir{#UseQNcyv z+z=cV7y-`ez^LE=a9#=y3yc6~dSFy=066;AAUHTi1UN6pC^s#_xWKxNcOF|RdCN>8 z5(iFC*}l;CnF)HF4wbxNvY95dYolp!j5u&+#wa%}!u*pU^SpOmD`=ko1}7ltSYF4% zssFkk^Zx%cmIL%Hd0l+a4%nigAcJynUNMLZ3IV5MP>?}6I9m Date: Mon, 29 Mar 2021 22:31:14 -0500 Subject: [PATCH 122/191] add obligation owner authority --- token-lending/program/src/error.rs | 3 +++ token-lending/program/src/instruction.rs | 18 ++++++++++++------ token-lending/program/src/processor.rs | 13 +++++++++++++ token-lending/program/src/state/obligation.rs | 11 +++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 3c2d828515f..dfbfaffc1d6 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -129,6 +129,9 @@ pub enum LendingError { ObligationLoanToValueLimit, // 35 + /// Expected a different obligation owner + #[error("Obligation owner is invalid")] + InvalidObligationOwner, /// Invalid obligation collateral #[error("Invalid obligation collateral")] InvalidObligationCollateral, diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 33b4adea987..14155f94038 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -133,9 +133,10 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Obligation account - uninitialized. /// 1. `[]` Lending market account. - /// 2. `[]` Clock sysvar. - /// 3. `[]` Rent sysvar. - /// 4. `[]` Token program id. + /// 2. `[signer]` Obligation owner. + /// 3. `[]` Clock sysvar. + /// 4. `[]` Rent sysvar. + /// 5. `[]` Token program id. InitObligation, // 7 @@ -214,9 +215,10 @@ pub enum LendingInstruction { /// 4. `[writable]` Obligation account - refreshed. /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. - /// 7. `[]` Clock sysvar. - /// 8. `[]` Token program id. - /// 9. `[optional, writable]` Host fee receiver account. + /// 7. `[signer]` Obligation owner. + /// 8. `[]` Clock sysvar. + /// 9. `[]` Token program id. + /// 10. `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power liquidity_amount: u64, @@ -685,12 +687,14 @@ pub fn init_obligation( program_id: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, ) -> Instruction { Instruction { program_id, accounts: vec![ AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), @@ -810,6 +814,7 @@ pub fn borrow_obligation_liquidity( borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, host_fee_receiver_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( @@ -824,6 +829,7 @@ pub fn borrow_obligation_liquidity( AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 2e63f23f020..e10ec549549 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -565,6 +565,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro let account_info_iter = &mut accounts.iter(); let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -580,9 +581,14 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } + if !obligation_owner_info.is_signer { + return Err(LendingError::InvalidSigner.into()); + } + obligation.init(InitObligationParams { current_slot: clock.slot, lending_market: *lending_market_info.key, + owner: *obligation_owner_info.key, deposits: Vec::with_capacity(0), borrows: Vec::with_capacity(0), }); @@ -965,6 +971,7 @@ fn process_borrow_obligation_liquidity( let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1014,6 +1021,12 @@ fn process_borrow_obligation_liquidity( if obligation.last_update < borrow_reserve.last_update { return Err(LendingError::ObligationStale.into()); } + if &obligation.owner != obligation_owner_info.key { + return Err(LendingError::InvalidObligationOwner.into()); + } + if !obligation_owner_info.is_signer { + return Err(LendingError::InvalidSigner.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index bfaf7980497..9c708106645 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -25,6 +25,8 @@ pub struct Obligation { 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 @@ -37,6 +39,8 @@ pub struct InitObligationParams { 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 @@ -55,6 +59,7 @@ impl Obligation { pub fn init(&mut self, params: InitObligationParams) { self.version = PROGRAM_VERSION; self.last_update = LastUpdate::new(params.current_slot); + self.owner = params.owner; self.deposits = params.deposits; self.borrows = params.borrows; } @@ -342,6 +347,7 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, + owner, deposits_len, borrows_len, data_flat, @@ -351,6 +357,7 @@ impl Pack for Obligation { 8, 1, PUBKEY_BYTES, + PUBKEY_BYTES, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -360,6 +367,7 @@ impl Pack for Obligation { *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()); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -398,6 +406,7 @@ impl Pack for Obligation { last_update_slot, last_update_stale, lending_market, + owner, deposits_len, borrows_len, data_flat, @@ -407,6 +416,7 @@ impl Pack for Obligation { 8, 1, PUBKEY_BYTES, + PUBKEY_BYTES, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -451,6 +461,7 @@ impl Pack for Obligation { stale: unpack_bool(last_update_stale)?, }, lending_market: Pubkey::new_from_array(*lending_market), + owner: Pubkey::new_from_array(*owner), deposits, borrows, }) From d24399e35511b1fa0cf3d12dff353e804320ab1f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 29 Mar 2021 22:32:01 -0500 Subject: [PATCH 123/191] add obligation collateral token mint --- token-lending/program/src/processor.rs | 5 +++- token-lending/program/src/state/obligation.rs | 23 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e10ec549549..7a012c3780d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -772,7 +772,7 @@ fn process_deposit_obligation_collateral( } obligation - .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? + .find_or_add_collateral_to_deposits(*deposit_reserve_info.key, *obligation_token_mint_info.key)? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -865,6 +865,9 @@ fn process_withdraw_obligation_collateral( } let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + if collateral.token_mint != obligation_token_mint_info.key { + return Err(LendingError::InvalidTokenMint.into()); + } if collateral.deposited_amount == 0 { return Err(LendingError::ObligationCollateralEmpty.into()); } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 9c708106645..2ea99d64e85 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -163,13 +163,17 @@ impl Obligation { pub fn find_or_add_collateral_to_deposits( &mut self, deposit_reserve: Pubkey, + token_mint: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) { + if self.deposits[collateral_index].token_mint != token_mint { + return Err(LendingError::InvalidTokenMint.into()); + } Ok(&mut self.deposits[collateral_index]) } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { Err(LendingError::ObligationReserveLimit.into()) } else { - let collateral = ObligationCollateral::new(deposit_reserve); + let collateral = ObligationCollateral::new(deposit_reserve, token_mint); self.deposits.push(collateral); Ok(self.deposits.last_mut().unwrap()) } @@ -230,6 +234,8 @@ impl IsInitialized for Obligation { pub struct ObligationCollateral { /// Reserve collateral is deposited to pub deposit_reserve: Pubkey, + /// Mint address of the tokens for this obligation collateral + pub token_mint: Pubkey, /// Amount of collateral deposited pub deposited_amount: u64, /// Collateral market value in quote currency @@ -238,9 +244,10 @@ pub struct ObligationCollateral { impl ObligationCollateral { /// Create new obligation collateral - pub fn new(deposit_reserve: Pubkey) -> Self { + pub fn new(deposit_reserve: Pubkey, token_mint: Pubkey) -> Self { Self { deposit_reserve, + token_mint, deposited_amount: 0, market_value: Decimal::zero(), } @@ -333,7 +340,7 @@ impl ObligationLiquidity { } // @TODO: adjust padding. what's a reasonable number? -const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 +const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 820; // 1 + 8 + 1 + 32 + 1 + 1 + (56 * 1) + (80 * 9) impl Pack for Obligation { @@ -375,9 +382,10 @@ impl Pack for Obligation { 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) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; + let (deposit_reserve, token_mint, deposited_amount, market_value) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); + token_mint.copy_from_slice(collateral.token_mint.as_ref()); *deposited_amount = collateral.deposited_amount.to_le_bytes(); pack_decimal(collateral.market_value, market_value); offset += OBLIGATION_COLLATERAL_LEN; @@ -431,10 +439,11 @@ impl Pack for Obligation { 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) = - array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; + let (deposit_reserve, token_mint, deposited_amount, market_value) = + array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; deposits.push(ObligationCollateral { deposit_reserve: Pubkey::new(deposit_reserve), + token_mint: Pubkey::new(token_mint), deposited_amount: u64::from_le_bytes(*deposited_amount), market_value: unpack_decimal(market_value), }); From ab995da65907f347644dc02b208fd016aa43962d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 29 Mar 2021 22:32:14 -0500 Subject: [PATCH 124/191] set new obligation length --- token-lending/program/src/state/obligation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 2ea99d64e85..0bb23cc2e7c 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -342,7 +342,7 @@ impl ObligationLiquidity { // @TODO: adjust padding. what's a reasonable number? const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 -const OBLIGATION_LEN: usize = 820; // 1 + 8 + 1 + 32 + 1 + 1 + (56 * 1) + (80 * 9) +const OBLIGATION_LEN: usize = 884; // 1 + 8 + 1 + 32 + 32 + 1 + 1 + (88 * 1) + (80 * 9) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; From 431b34d545c18e5eca73b25d27ab508a55b597c8 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 29 Mar 2021 22:32:27 -0500 Subject: [PATCH 125/191] remove unused function --- token-lending/program/src/instruction.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 14155f94038..1929869e000 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -379,25 +379,6 @@ impl LendingInstruction { } } - fn unpack_bool(input: &[u8]) -> Result<(bool, &[u8]), ProgramError> { - if !input.is_empty() { - let (byte, rest) = input.split_at(1); - let boolean = byte - .get(..1) - .and_then(|slice| slice.try_into().ok()) - .map(u8::from_le_bytes) - .and_then(|uint| match uint { - 0 => Some(false), - 1 => Some(true), - _ => None, - }) - .ok_or(LendingError::InstructionUnpackError)?; - Ok((boolean, rest)) - } else { - Err(LendingError::InstructionUnpackError.into()) - } - } - fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { if input.len() >= PUBKEY_BYTES { let (key, rest) = input.split_at(PUBKEY_BYTES); From 97e27c78f54843f9d0e37f9cf07eeb83a3314ee8 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 29 Mar 2021 22:32:35 -0500 Subject: [PATCH 126/191] bool -> boolean --- token-lending/program/src/state/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index f4b95f7748f..b6a6a3e0976 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -70,8 +70,8 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { Decimal::from_scaled_val(u128::from_le_bytes(*src)) } -fn pack_bool(bool: bool, dst: &mut [u8; 1]) { - *dst = (bool as u8).to_le_bytes() +fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { + *dst = (boolean as u8).to_le_bytes() } fn unpack_bool(src: &[u8; 1]) -> Result { From 0b05aaced1ca911bc34c109abfc3130bf2f84865 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 16:26:34 -0500 Subject: [PATCH 127/191] fmt --- token-lending/program/src/processor.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7a012c3780d..af29aa73194 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -772,7 +772,10 @@ fn process_deposit_obligation_collateral( } obligation - .find_or_add_collateral_to_deposits(*deposit_reserve_info.key, *obligation_token_mint_info.key)? + .find_or_add_collateral_to_deposits( + *deposit_reserve_info.key, + *obligation_token_mint_info.key, + )? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -864,7 +867,8 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::ObligationStale.into()); } - let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + let (collateral, collateral_index) = + obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; if collateral.token_mint != obligation_token_mint_info.key { return Err(LendingError::InvalidTokenMint.into()); } @@ -1176,7 +1180,8 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1316,8 +1321,10 @@ fn process_liquidate_obligation( return Err(LendingError::ObligationStale.into()); } - let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; - let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + let (collateral, collateral_index) = + obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), From 4aea356ee89a676febdedf51c79081846c5eecf3 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 16:31:25 -0500 Subject: [PATCH 128/191] allow same repay and withdraw currency on liquidation --- token-lending/program/src/error.rs | 26 +++++++++----------------- token-lending/program/src/processor.rs | 10 ---------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index dfbfaffc1d6..6cd7dc83b0b 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -76,14 +76,6 @@ pub enum LendingError { TokenBurnFailed, // 20 - // @TODO: this is only used in one place that might be removed. - /// Input reserves cannot be the same - #[error("Input reserves cannot be the same")] - DuplicateReserve, - // @TODO: this is only used in one place that might be removed. - /// Input reserves cannot use the same liquidity mint - #[error("Input reserves cannot use the same liquidity mint")] - DuplicateReserveMint, /// Insufficient liquidity available #[error("Insufficient liquidity available")] InsufficientLiquidity, @@ -93,14 +85,14 @@ pub enum LendingError { /// Reserve state stale #[error("Reserve state needs to be refreshed")] ReserveStale, - - // 25 /// 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, @@ -110,25 +102,20 @@ pub enum LendingError { /// Repay amount too small #[error("Repay amount too small to transfer liquidity")] RepayTooSmall, - - // 30 /// 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, - /// Obligation loan to value limit exceeded - #[error("Obligation loan to value limit exceeded")] - ObligationLoanToValueLimit, - - // 35 /// Expected a different obligation owner #[error("Obligation owner is invalid")] InvalidObligationOwner, @@ -138,12 +125,17 @@ pub enum LendingError { /// Invalid obligation liquidity #[error("Invalid obligation liquidity")] InvalidObligationLiquidity, + + // 35 /// Obligation collateral is empty #[error("Obligation collateral is empty")] ObligationCollateralEmpty, /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, + /// Obligation loan to value limit exceeded + #[error("Obligation loan to value limit exceeded")] + ObligationLoanToValueLimit, } impl From for ProgramError { diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index af29aa73194..14e31eb4894 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1293,16 +1293,6 @@ fn process_liquidate_obligation( return Err(LendingError::ReserveStale.into()); } - // @TODO: what if a user borrows using the same reserve & mint for collateral? - // this is permitted in borrow_obligation_liquidity and could be used for leverage. - // maybe liquidation should be allowed, but reduce/eliminate the bonus. - if repay_reserve_info.key == withdraw_reserve_info.key { - return Err(LendingError::DuplicateReserve.into()); - } - if repay_reserve.liquidity.mint_pubkey == withdraw_reserve.liquidity.mint_pubkey { - return Err(LendingError::DuplicateReserveMint.into()); - } - let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); From c940f034be186d5eb10ba0cb50f3a970cdf9dc2f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 16:33:59 -0500 Subject: [PATCH 129/191] comment addressed in review --- token-lending/program/src/processor.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 14e31eb4894..e557e5aac63 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -251,8 +251,6 @@ fn process_init_reserve( } else { let aggregator_info = next_account_info(account_info_iter)?; assert_rent_exempt(rent, aggregator_info)?; - - // @TODO: is there a way to check that aggregator_info represents the base:quote pair? ( COption::Some(*aggregator_info.key), read_median(aggregator_info)?.median, From f182b877e8b593504c892e136d8b1b260060206e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 16:43:29 -0500 Subject: [PATCH 130/191] fix clippy errors --- token-lending/program/src/instruction.rs | 8 ++++---- token-lending/program/src/math/decimal.rs | 2 +- token-lending/program/src/math/rate.rs | 2 +- token-lending/program/src/processor.rs | 4 ++-- token-lending/program/src/state/reserve.rs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 1929869e000..211640dc97d 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -196,7 +196,7 @@ pub enum LendingInstruction { /// 9. `[]` Clock sysvar. /// 10 `[]` Token program id. WithdrawObligationCollateral { - /// Amount of collateral to withdraw - u64::MAX for up to 100% of deposited amount + /// Amount of collateral to withdraw - u64::max_value() for up to 100% of deposited amount collateral_amount: u64, }, @@ -220,7 +220,7 @@ pub enum LendingInstruction { /// 9. `[]` Token program id. /// 10. `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { - /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power + /// Amount of liquidity to borrow - u64::max_value() for 100% of borrowing power liquidity_amount: u64, // @TODO: slippage constraint - https://git.io/JmV67 }, @@ -242,7 +242,7 @@ pub enum LendingInstruction { /// 7. `[]` Clock sysvar. /// 8. `[]` Token program id. RepayObligationLiquidity { - /// Amount of liquidity to repay - u64::MAX for 100% of borrowed amount + /// Amount of liquidity to repay - u64::max_value() for 100% of borrowed amount liquidity_amount: u64, }, @@ -268,7 +268,7 @@ pub enum LendingInstruction { /// 10 `[]` Clock sysvar. /// 11 `[]` Token program id. LiquidateObligation { - /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount + /// Amount of liquidity to repay - u64::max_value() for up to 100% of borrowed amount liquidity_amount: u64, }, } diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index a6da35bfdc3..1e4f3e60b9e 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -1,5 +1,5 @@ //! Math for preserving precision of token amounts which are limited -//! by the SPL Token program to be at most u64::MAX. +//! by the SPL Token program to be at most u64::max_value(). //! //! Decimals are internally scaled by a WAD (10^18) to preserve //! precision up to 18 decimal places. Decimals are sized to support diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 36674d835cc..033d4ce59e6 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -193,6 +193,6 @@ mod test { #[test] fn checked_pow() { - assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); + assert_eq!(Rate::one(), Rate::one().try_pow(u64::max_value()).unwrap()); } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e557e5aac63..746ce214f81 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -867,7 +867,7 @@ fn process_withdraw_obligation_collateral( let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - if collateral.token_mint != obligation_token_mint_info.key { + if &collateral.token_mint != obligation_token_mint_info.key { return Err(LendingError::InvalidTokenMint.into()); } if collateral.deposited_amount == 0 { @@ -907,7 +907,7 @@ fn process_withdraw_obligation_collateral( let max_withdraw_value = obligation.max_withdraw_value(loan_to_value_ratio)?; - let withdraw_amount = if collateral_amount == u64::MAX { + let withdraw_amount = if collateral_amount == u64::max_value() { let withdraw_value = max_withdraw_value.min(collateral.market_value); withdraw_value .try_div(collateral.market_value)? diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index d6c2fff1806..2be56eccd56 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -173,7 +173,7 @@ impl Reserve { liquidity_amount: u64, max_borrow_value: Decimal, ) -> Result { - if liquidity_amount == u64::MAX { + if liquidity_amount == u64::max_value() { let borrow_amount = max_borrow_value .try_div(self.liquidity.median_price)? .min(self.liquidity.available_amount.into()); @@ -221,7 +221,7 @@ impl Reserve { liquidity_amount: u64, borrow_amount: Decimal, ) -> Result { - let settle_amount = if liquidity_amount == u64::MAX { + let settle_amount = if liquidity_amount == u64::max_value() { borrow_amount } else { Decimal::from(liquidity_amount).min(borrow_amount) @@ -248,7 +248,7 @@ impl Reserve { ) -> Result { let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; - let liquidate_amount = if liquidity_amount == u64::MAX { + let liquidate_amount = if liquidity_amount == u64::max_value() { liquidity.borrowed_amount_wads } else { Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) From a206c25d80e2a2da363dea75029be0971199ff56 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 18:01:16 -0500 Subject: [PATCH 131/191] make order consistent --- token-lending/program/src/processor.rs | 4 ++-- token-lending/program/src/state/lending_market.rs | 6 +++--- token-lending/program/src/state/reserve.rs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 746ce214f81..ea9cdb8293d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -114,9 +114,9 @@ fn process_init_lending_market( lending_market.init(InitLendingMarketParams { bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, - owner, - quote_token_mint: *quote_token_mint_info.key, token_program_id: *token_program_id.key, + quote_token_mint: *quote_token_mint_info.key, + owner, }); LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 7ccdf956ba5..48ef765137b 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -45,9 +45,9 @@ impl LendingMarket { pub fn init(&mut self, params: InitLendingMarketParams) { self.version = PROGRAM_VERSION; self.bump_seed = params.bump_seed; - self.owner = params.owner; - self.quote_token_mint = params.quote_token_mint; self.token_program_id = params.token_program_id; + self.quote_token_mint = params.quote_token_mint; + self.owner = params.owner; } } @@ -79,7 +79,7 @@ impl Pack for LendingMarket { 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, quote_token_mint, token_program_id, owner, _padding) = + let (version, bump_seed, token_program_id, quote_token_mint, owner, _padding) = array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 2be56eccd56..b6d99e422dd 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -660,9 +660,9 @@ impl Pack for Reserve { liquidity_median_price, liquidity_available_amount, liquidity_borrowed_amount_wads, - collateral_supply, collateral_mint, collateral_mint_supply, + collateral_supply, config_optimal_utilization_rate, config_min_borrow_rate, config_optimal_borrow_rate, @@ -689,8 +689,8 @@ impl Pack for Reserve { 8, 16, PUBKEY_BYTES, - PUBKEY_BYTES, 8, + PUBKEY_BYTES, 1, 1, 1, @@ -726,8 +726,8 @@ impl Pack for Reserve { // collateral collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); + collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); // config *config_optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); @@ -759,9 +759,9 @@ impl Pack for Reserve { liquidity_median_price, liquidity_available_amount, liquidity_borrowed_amount_wads, - collateral_supply, collateral_mint, collateral_mint_supply, + collateral_supply, config_optimal_utilization_rate, config_min_borrow_rate, config_optimal_borrow_rate, @@ -771,7 +771,7 @@ impl Pack for Reserve { config_liquidation_bonus, config_fees_borrow_fee_wad, config_fees_host_fee_percentage, - __padding, + _padding, ) = array_refs![ input, 1, @@ -788,8 +788,8 @@ impl Pack for Reserve { 8, 16, PUBKEY_BYTES, - PUBKEY_BYTES, 8, + PUBKEY_BYTES, 1, 1, 1, From 719667616668ba2c83a450afb2bcc17cf3f3b7ef Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 19:36:06 -0500 Subject: [PATCH 132/191] mark reserves stale on updates --- token-lending/program/src/processor.rs | 6 ++++++ token-lending/program/src/state/reserve.rs | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ea9cdb8293d..f206026df50 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -371,6 +371,7 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro } reserve.accrue_interest(clock.slot)?; + reserve.last_update.update_slot(clock.slot); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; Ok(()) @@ -444,6 +445,7 @@ fn process_deposit_reserve_liquidity( } let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; + reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { @@ -535,6 +537,7 @@ fn process_redeem_reserve_collateral( } let liquidity_amount = reserve.redeem_collateral(collateral_amount)?; + reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { @@ -1065,6 +1068,7 @@ fn process_borrow_obligation_liquidity( } borrow_reserve.liquidity.borrow(borrow_amount)?; + borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; obligation @@ -1201,6 +1205,7 @@ fn process_repay_obligation_liquidity( } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; obligation.repay(settle_amount, liquidity_index)?; @@ -1348,6 +1353,7 @@ fn process_liquidate_obligation( } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; obligation.repay(settle_amount, liquidity_index)?; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b6d99e422dd..83282b0be82 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -25,7 +25,6 @@ pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2; pub struct Reserve { /// Version of the struct pub version: u8, - // @TODO: check to see if `last_update` is changed when supply changes /// Last slot when supply and rates updated pub last_update: LastUpdate, /// Lending market address @@ -162,7 +161,6 @@ impl Reserve { let current_borrow_rate = self.current_borrow_rate()?; self.liquidity .compound_interest(current_borrow_rate, slots_elapsed)?; - self.last_update.update_slot(current_slot); } Ok(()) } From 0b44ceeec5f74cf7b7db48f901e1018063615f88 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 19:36:20 -0500 Subject: [PATCH 133/191] add helper methods --- token-lending/program/src/state/reserve.rs | 66 ++++++++++++++-------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 83282b0be82..b0d3a34d844 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -75,16 +75,8 @@ impl Reserve { .collateral_exchange_rate()? .liquidity_to_collateral(liquidity_amount)?; - self.liquidity.available_amount = self - .liquidity - .available_amount - .checked_add(liquidity_amount) - .ok_or(LendingError::MathOverflow)?; - self.collateral.mint_total_supply = self - .collateral - .mint_total_supply - .checked_add(collateral_amount) - .ok_or(LendingError::MathOverflow)?; + self.liquidity.deposit(liquidity_amount)?; + self.collateral.mint(collateral_amount)?; Ok(collateral_amount) } @@ -94,20 +86,9 @@ impl Reserve { let collateral_exchange_rate = self.collateral_exchange_rate()?; let liquidity_amount = collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?; - if liquidity_amount > self.liquidity.available_amount { - return Err(LendingError::InsufficientLiquidity.into()); - } - self.liquidity.available_amount = self - .liquidity - .available_amount - .checked_sub(liquidity_amount) - .ok_or(LendingError::MathOverflow)?; - self.collateral.mint_total_supply = self - .collateral - .mint_total_supply - .checked_sub(collateral_amount) - .ok_or(LendingError::MathOverflow)?; + self.collateral.burn(collateral_amount)?; + self.liquidity.withdraw(liquidity_amount)?; Ok(liquidity_amount) } @@ -396,6 +377,27 @@ impl ReserveLiquidity { Decimal::from(self.available_amount).try_add(self.borrowed_amount_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 { + 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_amount: Decimal) -> ProgramResult { let receive_amount = borrow_amount.try_floor_u64()?; @@ -481,6 +483,24 @@ impl ReserveCollateral { } } + /// 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, From 9d576b01d3b2b0625fa60facf7ba32dbeb7ce992 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 30 Mar 2021 21:58:15 -0500 Subject: [PATCH 134/191] fix missing set --- token-lending/program/src/state/obligation.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 0bb23cc2e7c..51b71022be1 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -59,6 +59,7 @@ impl 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; From 47f50c6732f587ff3efea71527ad4515fb8f9b9d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 1 Apr 2021 20:25:18 -0500 Subject: [PATCH 135/191] lending market not needed on obligation refresh --- token-lending/program/src/instruction.rs | 7 +------ token-lending/program/src/processor.rs | 22 ---------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 211640dc97d..c0784fd55eb 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -147,9 +147,7 @@ pub enum LendingInstruction { /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation account. - /// 1. `[]` Lending market account. - /// 2. `[]` Clock sysvar. - /// 3. `[]` Token program id. + /// 1. `[]` Clock sysvar. /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. RefreshObligation, @@ -689,14 +687,11 @@ pub fn init_obligation( pub fn refresh_obligation( program_id: Pubkey, obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, reserve_pubkeys: Vec, ) -> Instruction { let mut accounts = vec![ AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), ]; accounts.extend( reserve_pubkeys diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f206026df50..ea7548261ea 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -601,26 +601,12 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - return Err(LendingError::InvalidTokenProgram.into()); - } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } for collateral in &mut obligation.deposits { let deposit_reserve_info = next_account_info(account_info_iter)?; @@ -633,10 +619,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> } let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid deposit reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } if deposit_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -658,10 +640,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> } let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Invalid borrow reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); - } if borrow_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } From 49c582157a5b173d42dd146e84d26358276a9d23 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 1 Apr 2021 23:46:37 -0500 Subject: [PATCH 136/191] add fixme comment for collateral withdraw --- token-lending/program/src/processor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ea7548261ea..1d5863fd2b0 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -890,14 +890,15 @@ fn process_withdraw_obligation_collateral( let withdraw_amount = if collateral_amount == u64::max_value() { let withdraw_value = max_withdraw_value.min(collateral.market_value); - withdraw_value - .try_div(collateral.market_value)? + let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; + withdraw_pct .try_mul(collateral.deposited_amount)? .try_floor_u64()? .min(collateral.deposited_amount) } else { let withdraw_amount = collateral_amount.min(collateral.deposited_amount); let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + // @FIXME: this doesn't look correct, withdraw_pct only applies to this collateral let withdraw_value = obligation.deposited_value()?.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { return Err(LendingError::WithdrawTooLarge.into()); From ad48cfa3cfbca6666383fbe0f1c06587a73fe1f5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 1 Apr 2021 23:47:05 -0500 Subject: [PATCH 137/191] convert quote price to base decimals --- token-lending/program/src/state/reserve.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b0d3a34d844..17fa18ac82c 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -153,8 +153,11 @@ impl Reserve { max_borrow_value: Decimal, ) -> Result { if liquidity_amount == u64::max_value() { + let decimals = 10u64.checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; let borrow_amount = max_borrow_value .try_div(self.liquidity.median_price)? + .try_mul(decimals)? .min(self.liquidity.available_amount.into()); let (origination_fee, host_fee) = self .config From 501b86999deb332bdd539276e241bef064e0b5cc Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 2 Apr 2021 23:29:22 -0500 Subject: [PATCH 138/191] remove unused account infos --- token-lending/program/src/instruction.rs | 5 ----- token-lending/program/src/processor.rs | 14 +------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index c0784fd55eb..9e1dcf85737 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -831,10 +831,6 @@ pub fn repay_obligation_liquidity( lending_market_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, - ); Instruction { program_id, accounts: vec![ @@ -843,7 +839,6 @@ pub fn repay_obligation_liquidity( AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 1d5863fd2b0..6d6a36bcc8b 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1110,10 +1110,8 @@ fn process_repay_obligation_liquidity( let source_liquidity_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1164,16 +1162,6 @@ fn process_repay_obligation_liquidity( let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { - return Err(LendingError::InvalidMarketAuthority.into()); - } - let RepayLiquidityResult { settle_amount, repay_amount, @@ -1193,7 +1181,7 @@ fn process_repay_obligation_liquidity( spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), - destination: repay_reserve_liquidity_supply_info.clone(), + destination: destination_liquidity_info.clone(), amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], From 63065f0c47f92f1de82095f3f30535dd5a67d1e2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 2 Apr 2021 23:30:04 -0500 Subject: [PATCH 139/191] do conversion for currency decimals * market price --- token-lending/program/src/processor.rs | 10 ++++++++++ token-lending/program/src/state/reserve.rs | 9 ++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6d6a36bcc8b..838e6db8300 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -623,9 +623,14 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ReserveStale.into()); } + let decimals = 10u64 + .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + collateral.market_value = deposit_reserve .collateral_exchange_rate()? .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? + .try_div(decimals)? .try_mul(deposit_reserve.liquidity.median_price)?; } @@ -644,9 +649,14 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ReserveStale.into()); } + let decimals = 10u64 + .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; liquidity.market_value = liquidity .borrowed_amount_wads + .try_div(decimals)? .try_mul(borrow_reserve.liquidity.median_price)?; } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 17fa18ac82c..3a8bd7cfdba 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -152,9 +152,10 @@ impl Reserve { liquidity_amount: u64, max_borrow_value: Decimal, ) -> Result { + let decimals = 10u64 + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; if liquidity_amount == u64::max_value() { - let decimals = 10u64.checked_pow(self.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; let borrow_amount = max_borrow_value .try_div(self.liquidity.median_price)? .try_mul(decimals)? @@ -183,7 +184,9 @@ impl Reserve { .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; - let borrow_value = borrow_amount.try_mul(self.liquidity.median_price)?; + let borrow_value = borrow_amount + .try_div(decimals)? + .try_mul(self.liquidity.median_price)?; if borrow_value > max_borrow_value { return Err(LendingError::BorrowTooLarge.into()); } From b9bbc727b0d385c9f7fea0a55b713915de6481a0 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 2 Apr 2021 23:30:37 -0500 Subject: [PATCH 140/191] fix withdraw value calculation --- token-lending/program/src/processor.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 838e6db8300..5eea1119e41 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -908,8 +908,7 @@ fn process_withdraw_obligation_collateral( } else { let withdraw_amount = collateral_amount.min(collateral.deposited_amount); let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - // @FIXME: this doesn't look correct, withdraw_pct only applies to this collateral - let withdraw_value = obligation.deposited_value()?.try_mul(withdraw_pct)?; + let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { return Err(LendingError::WithdrawTooLarge.into()); } From ff4cf6cc1420409296046a52f2e7e9f508c3d75c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 2 Apr 2021 23:30:53 -0500 Subject: [PATCH 141/191] remove superfluous type --- token-lending/program/src/state/reserve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3a8bd7cfdba..d5d11ab1fe0 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -446,7 +446,7 @@ impl ReserveLiquidity { current_borrow_rate: Rate, slots_elapsed: u64, ) -> ProgramResult { - let slot_interest_rate: Rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?; + 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)?; From be3d1ced5c790ab275768fb86fa7f0198ffd47ac Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 5 Apr 2021 17:31:43 -0500 Subject: [PATCH 142/191] adjust liquidation math --- token-lending/program/src/processor.rs | 2 +- token-lending/program/src/state/reserve.rs | 91 ++++++++++++---------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 5eea1119e41..f7cc6020a5f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1306,7 +1306,7 @@ fn process_liquidate_obligation( } // @FIXME: LTV - let liquidation_threshold = Rate::from_percent(55); + let liquidation_threshold = Rate::from_percent(80); // let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); let loan_to_value = obligation.loan_to_value()?; if loan_to_value < liquidation_threshold { diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index d5d11ab1fe0..b3d64cd91ae 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -233,53 +233,64 @@ impl Reserve { ) -> Result { let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; - let liquidate_amount = if liquidity_amount == u64::max_value() { + let target_amount = if liquidity_amount == u64::max_value() { liquidity.borrowed_amount_wads } else { Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) }; - let (settle_amount, settle_pct, repay_amount) = - if liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() { - // Close out obligations that are too small to liquidate normally - let settle_amount = liquidity.borrowed_amount_wads; - let settle_value = liquidity - .market_value - .try_mul(bonus_rate)? - .min(collateral.market_value); - let settle_pct = settle_value.try_div(collateral.market_value)?; - let repay_amount = liquidate_amount.min(settle_amount).try_ceil_u64()?; - - (settle_amount, settle_pct, repay_amount) + let settle_amount; + let repay_amount; + let withdraw_amount; + + // Close out obligations that are too small to liquidate normally + if liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() { + // settle_amount is fixed, calculate withdraw_amount and repay_amount + settle_amount = liquidity.borrowed_amount_wads; + + let liquidation_value = liquidity.market_value.try_mul(bonus_rate)?; + if liquidation_value > collateral.market_value { + let repay_pct = collateral.market_value.try_div(liquidation_value)?; + repay_amount = target_amount.try_mul(repay_pct)?.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } else if liquidation_value == collateral.market_value { + repay_amount = target_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; } else { - let max_liquidation_amount = obligation.max_liquidation_amount(liquidity)?; - let liquidation_amount = liquidate_amount.min(max_liquidation_amount); - let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?; - - let settle_value = liquidity - .market_value - .try_mul(liquidation_pct)? - .try_mul(bonus_rate)? - .min(collateral.market_value); - let settle_pct = settle_value.try_div(collateral.market_value)?; - let settle_amount = liquidation_amount.try_mul(settle_pct)?; - - let repay_amount = if settle_amount == max_liquidation_amount { - settle_amount.try_ceil_u64()? - } else { - settle_amount.try_floor_u64()? - }; - - (settle_amount, settle_pct, repay_amount) - }; - - let max_withdraw_amount = Decimal::from(collateral.deposited_amount); - let collateral_amount = max_withdraw_amount.try_mul(settle_pct)?; - let withdraw_amount = if collateral_amount == max_withdraw_amount { - collateral_amount.try_ceil_u64()? + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + repay_amount = target_amount.try_ceil_u64()?; + withdraw_amount = Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_ceil_u64()?; + } } else { - collateral_amount.try_floor_u64()? - }; + // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded up + let liquidation_amount = obligation + .max_liquidation_amount(liquidity)? + .min(target_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)?; + + if liquidation_value > collateral.market_value { + let repay_pct = collateral.market_value.try_div(liquidation_value)?; + settle_amount = liquidation_amount.try_mul(repay_pct)?; + withdraw_amount = collateral.deposited_amount; + } else if liquidation_value == collateral.market_value { + settle_amount = liquidation_amount; + withdraw_amount = collateral.deposited_amount; + } else { + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + settle_amount = liquidation_amount; + withdraw_amount = Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_ceil_u64()?; + } + + repay_amount = settle_amount.try_ceil_u64()?; + } Ok(LiquidateObligationResult { settle_amount, From 6c81fdbd583cc145c780b15e62533605886b38e5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Mon, 5 Apr 2021 17:32:07 -0500 Subject: [PATCH 143/191] change STALE_AFTER_SLOTS semantics --- token-lending/program/src/state/last_update.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/program/src/state/last_update.rs index 86a3097541f..68f95de7799 100644 --- a/token-lending/program/src/state/last_update.rs +++ b/token-lending/program/src/state/last_update.rs @@ -3,7 +3,7 @@ use solana_program::{clock::Slot, program_error::ProgramError}; use std::cmp::Ordering; /// Number of slots to consider stale after -pub const STALE_AFTER_SLOTS: u64 = 0; +pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1; /// Last update state #[derive(Clone, Debug, Default)] @@ -39,9 +39,9 @@ impl LastUpdate { self.stale = true; } - /// Check if last update slot is too long ago + /// 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) + Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED) } } From 2d81b54b8cf4209c10ed4f4be1c364a0ec079339 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 6 Apr 2021 15:10:42 -0500 Subject: [PATCH 144/191] init obligation token mint and output on deposit --- token-lending/program/src/instruction.rs | 19 +++++---- token-lending/program/src/processor.rs | 49 ++++++++++++++++++------ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 9e1dcf85737..9cfda27aca4 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -167,9 +167,11 @@ pub enum LendingInstruction { /// 5. `[writable]` Obligation token output account. /// 6. `[]` Lending market account. /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` User transfer authority ($authority). - /// 9. `[]` Clock sysvar. - /// 10 `[]` Token program id. + /// 8. `[signer]` Obligation owner. + /// 9. `[signer]` User transfer authority ($authority). + /// 10 `[]` Clock sysvar. + /// 11 `[]` Rent sysvar. + /// 12 `[]` Token program id. DepositObligationCollateral { /// Amount of collateral to deposit collateral_amount: u64, @@ -714,9 +716,10 @@ pub fn deposit_obligation_collateral( destination_collateral_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_mint_pubkey: Pubkey, - obligation_output_pubkey: Pubkey, + obligation_token_mint_pubkey: Pubkey, + obligation_token_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( @@ -730,12 +733,14 @@ pub fn deposit_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_mint_pubkey, false), - AccountMeta::new(obligation_output_pubkey, false), + AccountMeta::new(obligation_token_mint_pubkey, false), + AccountMeta::new(obligation_token_output_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f7cc6020a5f..ec0d8e3a460 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -690,8 +690,10 @@ fn process_deposit_obligation_collateral( let obligation_token_output_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent_info = next_account_info(account_info_iter)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -718,9 +720,6 @@ fn process_deposit_obligation_collateral( msg!("Invalid source collateral account"); return Err(LendingError::InvalidAccountInput.into()); } - // @TODO: is this necessary? we don't care about market price or interest here yet. - // however, we will if we add the ability to deposit and borrow in one transaction. - // it would also be important if deposit_reserve.config.collateral_enabled changes. if deposit_reserve.last_update.is_stale(clock.slot)? { return Err(LendingError::ReserveStale.into()); } @@ -736,18 +735,46 @@ fn process_deposit_obligation_collateral( msg!("Invalid obligation lending market account"); return Err(LendingError::InvalidAccountInput.into()); } + if &obligation.owner != obligation_owner_info.key { + return Err(LendingError::InvalidObligationOwner.into()); + } + if !obligation_owner_info.is_signer { + return Err(LendingError::InvalidSigner.into()); + } - unpack_mint(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); + let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint.is_initialized() { + if obligation_token_mint_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } + } + else { + spl_token_init_mint(TokenInitializeMintParams { + mint: obligation_token_mint_info.clone(), + authority: lending_market_authority_info.key, + rent: rent_info.clone(), + decimals: deposit_reserve.liquidity.mint_decimals, + token_program: token_program_id.clone(), + })?; } - let obligation_token_output = Account::unpack(&obligation_token_output_info.data.borrow())?; - if obligation_token_output_info.owner != token_program_id.key { - return Err(LendingError::InvalidTokenOwner.into()); + let obligation_token_output = Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; + if obligation_token_output.is_initialized() { + if obligation_token_output_info.owner != token_program_id.key { + return Err(LendingError::InvalidTokenOwner.into()); + } + if &obligation_token_output.mint != obligation_token_mint_info.key { + return Err(LendingError::InvalidTokenMint.into()); + } } - if &obligation_token_output.mint != obligation_token_mint_info.key { - return Err(LendingError::InvalidTokenMint.into()); + else { + spl_token_init_account(TokenInitializeAccountParams { + account: obligation_token_output_info.clone(), + mint: obligation_token_mint_info.clone(), + owner: obligation_owner_info.clone(), + rent: rent_info.clone(), + token_program: token_program_id.clone(), + })?; } let authority_signer_seeds = &[ From 4af2ecbe12a09db53736b94dc19ef895fd096051 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 6 Apr 2021 15:48:15 -0500 Subject: [PATCH 145/191] add additional safety checks --- token-lending/program/src/processor.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ec0d8e3a460..31aa9780986 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -742,11 +742,15 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidSigner.into()); } + // @TODO: does there need to be a check to make sure obligation_token_mint_info is rent exempt? let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint.is_initialized() { if obligation_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } + if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { + return Err(LendingError::InvalidMarketAuthority.into()); + } } else { spl_token_init_mint(TokenInitializeMintParams { @@ -758,6 +762,7 @@ fn process_deposit_obligation_collateral( })?; } + // @TODO: does there need to be a check to make sure obligation_token_output_info is rent exempt? let obligation_token_output = Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; if obligation_token_output.is_initialized() { if obligation_token_output_info.owner != token_program_id.key { @@ -766,6 +771,9 @@ fn process_deposit_obligation_collateral( if &obligation_token_output.mint != obligation_token_mint_info.key { return Err(LendingError::InvalidTokenMint.into()); } + if &obligation_token_output.owner != obligation_owner_info.key { + return Err(LendingError::InvalidObligationOwner.into()); + } } else { spl_token_init_account(TokenInitializeAccountParams { From fd7f88f28fb8812f988e3744b4e30157edaadb9e Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:10:00 -0500 Subject: [PATCH 146/191] add more errors for deposits and borrows --- token-lending/program/src/error.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 6cd7dc83b0b..daab737a949 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -119,23 +119,34 @@ pub enum LendingError { /// 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, - - // 35 /// Obligation collateral is empty #[error("Obligation collateral is empty")] ObligationCollateralEmpty, + + // 40 /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, - /// Obligation loan to value limit exceeded - #[error("Obligation loan to value limit exceeded")] - ObligationLoanToValueLimit, } impl From for ProgramError { From b60b6be5b223d590986896141cd65d2b64c25ae9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:21:07 -0500 Subject: [PATCH 147/191] implement per-collateral LTV --- token-lending/program/src/processor.rs | 140 ++++++++++++------ token-lending/program/src/state/mod.rs | 15 +- token-lending/program/src/state/obligation.rs | 93 ++++++------ 3 files changed, 158 insertions(+), 90 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 31aa9780986..15074c86e14 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -3,7 +3,7 @@ use crate::{ error::LendingError, instruction::LendingInstruction, - math::{Decimal, Rate, TryDiv, TryMul, WAD}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, WAD}, state::{ BorrowLiquidityResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, LiquidateObligationResult, NewReserveCollateralParams, @@ -26,6 +26,7 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::{Account, Mint}; +use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( @@ -608,6 +609,11 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountOwner.into()); } + let mut deposited_value = Decimal::zero(); + let mut borrowed_value = Decimal::zero(); + let mut loan_to_value_ratio = Decimal::zero(); + let mut liquidation_threshold = Decimal::zero(); + for collateral in &mut obligation.deposits { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { @@ -627,11 +633,22 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; - collateral.market_value = deposit_reserve + let market_value = deposit_reserve .collateral_exchange_rate()? .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? - .try_div(decimals)? - .try_mul(deposit_reserve.liquidity.median_price)?; + .try_mul(deposit_reserve.liquidity.median_price)? + .try_div(decimals)?; + collateral.market_value = market_value; + + let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); + let liquidation_threshold_rate = + Rate::from_percent(deposit_reserve.config.liquidation_threshold); + + deposited_value = deposited_value.try_add(market_value)?; + loan_to_value_ratio = + loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; + liquidation_threshold = + liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } for liquidity in &mut obligation.borrows { @@ -654,10 +671,12 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .ok_or(LendingError::MathOverflow)?; liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; - liquidity.market_value = liquidity + let market_value = liquidity .borrowed_amount_wads - .try_div(decimals)? - .try_mul(borrow_reserve.liquidity.median_price)?; + .try_mul(borrow_reserve.liquidity.median_price)? + .try_div(decimals)?; + liquidity.market_value = market_value; + borrowed_value = borrowed_value.try_add(market_value)?; } if account_info_iter.peek().is_some() { @@ -665,6 +684,19 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } + obligation.deposited_value = deposited_value; + obligation.borrowed_value = borrowed_value; + + if deposited_value == Decimal::zero() { + obligation.loan_to_value_ratio = Rate::zero(); + obligation.liquidation_threshold = Rate::zero(); + } else { + obligation.loan_to_value_ratio = + Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; + obligation.liquidation_threshold = + Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; + } + obligation.last_update.update_slot(clock.slot); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -923,35 +955,44 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - // @FIXME: LTV - let loan_to_value_ratio = Rate::from_percent(50); - // let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); - let loan_to_value = obligation.loan_to_value()?; - if loan_to_value >= loan_to_value_ratio { - return Err(LendingError::ObligationLoanToValueLimit.into()); - } - - let max_withdraw_value = obligation.max_withdraw_value(loan_to_value_ratio)?; - - let withdraw_amount = if collateral_amount == u64::max_value() { - let withdraw_value = max_withdraw_value.min(collateral.market_value); - let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; - withdraw_pct - .try_mul(collateral.deposited_amount)? - .try_floor_u64()? - .min(collateral.deposited_amount) + let withdraw_amount = if obligation.borrows.is_empty() { + // there are no borrows; they have been repaid, liquidated, or were never taken out + collateral.deposited_amount + } else if obligation.borrowed_value == Decimal::zero() { + // there are borrows, but they cannot be valued; they must be repaid to withdraw collateral + return Err(LendingError::ObligationBorrowsZero.into()); + } else if obligation.deposited_value == Decimal::zero() { + // there are deposits, but they cannot be valued + return Err(LendingError::ObligationDepositsZero.into()); } else { - let withdraw_amount = collateral_amount.min(collateral.deposited_amount); - let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { + // there are borrows and deposits, and they can both be valued + let max_withdraw_value = obligation.max_withdraw_value()?; + if max_withdraw_value == Decimal::zero() { return Err(LendingError::WithdrawTooLarge.into()); } + + let withdraw_amount = if collateral_amount == u64::max_value() { + let withdraw_value = max_withdraw_value.min(collateral.market_value); + let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; + withdraw_pct + .try_mul(collateral.deposited_amount)? + .try_floor_u64()? + .min(collateral.deposited_amount) + } else { + let withdraw_amount = collateral_amount.min(collateral.deposited_amount); + let withdraw_pct = + Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + return Err(LendingError::WithdrawTooLarge.into()); + } + withdraw_amount + }; + if withdraw_amount == 0 { + return Err(LendingError::WithdrawTooSmall.into()); + } withdraw_amount }; - if withdraw_amount == 0 { - return Err(LendingError::WithdrawTooSmall.into()); - } let obligation_token_amount = collateral .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; @@ -1058,6 +1099,12 @@ fn process_borrow_obligation_liquidity( if !obligation_owner_info.is_signer { return Err(LendingError::InvalidSigner.into()); } + if obligation.deposits.is_empty() { + return Err(LendingError::ObligationDepositsEmpty.into()); + } + if obligation.deposited_value == Decimal::zero() { + return Err(LendingError::ObligationDepositsZero.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1069,16 +1116,11 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - // @FIXME: LTV - let loan_to_value_ratio = Rate::from_percent(50); - // let loan_to_value_ratio = Rate::from_percent(lending_market.loan_to_value_ratio); - let loan_to_value = obligation.loan_to_value()?; - if loan_to_value >= loan_to_value_ratio { - return Err(LendingError::ObligationLoanToValueLimit.into()); + let max_borrow_value = obligation.max_borrow_value()?; + if max_borrow_value == Decimal::zero() { + return Err(LendingError::BorrowTooLarge.into()); } - let max_borrow_value = obligation.max_borrow_value(loan_to_value_ratio)?; - let BorrowLiquidityResult { borrow_amount, receive_amount, @@ -1205,6 +1247,9 @@ fn process_repay_obligation_liquidity( let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + if liquidity.borrowed_amount_wads == Decimal::zero() { + return Err(LendingError::ObligationLiquidityEmpty.into()); + } let RepayLiquidityResult { settle_amount, @@ -1324,6 +1369,15 @@ fn process_liquidate_obligation( if obligation.last_update < withdraw_reserve.last_update { return Err(LendingError::ObligationStale.into()); } + if obligation.deposited_value == Decimal::zero() { + return Err(LendingError::ObligationDepositsZero.into()); + } + if obligation.borrowed_value == Decimal::zero() { + return Err(LendingError::ObligationBorrowsZero.into()); + } + if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { + return Err(LendingError::ObligationHealthy.into()); + } let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; @@ -1340,14 +1394,6 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - // @FIXME: LTV - let liquidation_threshold = Rate::from_percent(80); - // let liquidation_threshold = Rate::from_percent(lending_market.liquidation_threshold); - let loan_to_value = obligation.loan_to_value()?; - if loan_to_value < liquidation_threshold { - return Err(LendingError::ObligationHealthy.into()); - } - let LiquidateObligationResult { settle_amount, repay_amount, diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index b6a6a3e0976..006718aad81 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -10,7 +10,7 @@ pub use lending_market::*; pub use obligation::*; pub use reserve::*; -use crate::math::{Decimal, WAD}; +use crate::math::{Decimal, Rate, WAD}; use arrayref::{array_refs, mut_array_refs}; use solana_program::{ clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, @@ -70,6 +70,19 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { Decimal::from_scaled_val(u128::from_le_bytes(*src)) } +// @TODO: Rate::to_scaled_val doesn't work here because it requires 16 bytes. +// Is it a problem to round to u64? +fn pack_rate(rate: Rate, dst: &mut [u8; 8]) { + *dst = rate + .try_round_u64() + .expect("could not pack rate") + .to_le_bytes(); +} + +fn unpack_rate(src: &[u8; 8]) -> Rate { + Rate::from_scaled_val(u64::from_le_bytes(*src)) +} + fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { *dst = (boolean as u8).to_le_bytes() } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 51b71022be1..b8e5823c3fa 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -31,6 +31,14 @@ pub struct Obligation { pub deposits: Vec, /// Borrowed liquidity for the obligation, unique by borrow reserve address pub borrows: Vec, + /// Market value of deposits + pub deposited_value: Decimal, + /// Market value of borrows + pub borrowed_value: Decimal, + /// The target ratio of borrowed value to deposited value + pub loan_to_value_ratio: Rate, + /// The loan to value ratio at which the obligation can be liquidated + pub liquidation_threshold: Rate, } /// Initialize an obligation @@ -87,50 +95,27 @@ impl Obligation { Ok(()) } - // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, - // but that seems sloppy. - /// Calculate the deposited collateral market value - pub fn deposited_value(&self) -> Result { - let mut deposited_value = Decimal::zero(); - for collateral in &self.deposits { - deposited_value = deposited_value.try_add(collateral.market_value)?; - } - Ok(deposited_value) - } - - // @TODO: this gets called a lot. we could persist the value on obligation refresh instead, - // but that seems sloppy. - /// Calculate the borrowed liquidity market value - pub fn borrowed_value(&self) -> Result { - let mut borrowed_value = Decimal::zero(); - for liquidity in &self.borrows { - borrowed_value = borrowed_value.try_add(liquidity.market_value)?; - } - Ok(borrowed_value) + /// 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) } - /// Calculate the ratio of liquidity market value to collateral market value - pub fn loan_to_value(&self) -> Result { - let deposited_value = self.deposited_value()?; - if deposited_value == Decimal::zero() { - return Err(LendingError::ObligationCollateralEmpty.into()); - } - Rate::try_from(self.borrowed_value()?.try_div(deposited_value)?) - } - - // @FIXME: LTV /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio - pub fn max_withdraw_value(&self, loan_to_value_ratio: Rate) -> Result { - let min_deposited_value = self.borrowed_value()?.try_div(loan_to_value_ratio)?; - self.deposited_value()?.try_sub(min_deposited_value) + pub fn max_withdraw_value(&self) -> Result { + let min_deposited_value = self.borrowed_value.try_div(self.loan_to_value_ratio)?; + if min_deposited_value >= self.deposited_value { + return Ok(Decimal::zero()); + } + self.deposited_value.try_sub(min_deposited_value) } - // @FIXME: LTV /// Calculate the maximum liquidity value that can be borrowed for a given loan to value ratio - pub fn max_borrow_value(&self, loan_to_value_ratio: Rate) -> Result { - self.deposited_value()? - .try_mul(loan_to_value_ratio)? - .try_sub(self.borrowed_value()?) + pub fn max_borrow_value(&self) -> Result { + let max_borrowed_value = self.deposited_value.try_mul(self.loan_to_value_ratio)?; + if self.borrowed_value >= max_borrowed_value { + return Ok(Decimal::zero()); + } + max_borrowed_value.try_sub(self.borrowed_value) } /// Calculate the maximum liquidation amount for a given liquidity @@ -139,7 +124,7 @@ impl Obligation { liquidity: &ObligationLiquidity, ) -> Result { let max_liquidation_value = self - .borrowed_value()? + .borrowed_value .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? .min(liquidity.market_value); let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; @@ -152,7 +137,7 @@ impl Obligation { deposit_reserve: Pubkey, ) -> Result<(&ObligationCollateral, usize), ProgramError> { if self.deposits.is_empty() { - return Err(LendingError::ObligationCollateralEmpty.into()); + return Err(LendingError::ObligationDepositsEmpty.into()); } let collateral_index = self ._find_collateral_index_in_deposits(deposit_reserve) @@ -192,7 +177,7 @@ impl Obligation { borrow_reserve: Pubkey, ) -> Result<(&ObligationLiquidity, usize), ProgramError> { if self.borrows.is_empty() { - return Err(LendingError::ObligationLiquidityEmpty.into()); + return Err(LendingError::ObligationBorrowsEmpty.into()); } let liquidity_index = self ._find_liquidity_index_in_borrows(borrow_reserve) @@ -343,7 +328,7 @@ impl ObligationLiquidity { // @TODO: adjust padding. what's a reasonable number? const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 -const OBLIGATION_LEN: usize = 884; // 1 + 8 + 1 + 32 + 32 + 1 + 1 + (88 * 1) + (80 * 9) +const OBLIGATION_LEN: usize = 932; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 8 + 8 + 1 + 1 + (88 * 1) + (80 * 9) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -356,6 +341,10 @@ impl Pack for Obligation { last_update_stale, lending_market, owner, + deposited_value, + borrowed_value, + loan_to_value_ratio, + liquidation_threshold, deposits_len, borrows_len, data_flat, @@ -366,6 +355,10 @@ impl Pack for Obligation { 1, PUBKEY_BYTES, PUBKEY_BYTES, + 16, + 16, + 8, + 8, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -376,6 +369,10 @@ impl Pack for Obligation { 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_rate(self.loan_to_value_ratio, loan_to_value_ratio); + pack_rate(self.liquidation_threshold, liquidation_threshold); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -416,6 +413,10 @@ impl Pack for Obligation { last_update_stale, lending_market, owner, + deposited_value, + borrowed_value, + loan_to_value_ratio, + liquidation_threshold, deposits_len, borrows_len, data_flat, @@ -426,6 +427,10 @@ impl Pack for Obligation { 1, PUBKEY_BYTES, PUBKEY_BYTES, + 16, + 16, + 8, + 8, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -474,6 +479,10 @@ impl Pack for Obligation { owner: Pubkey::new_from_array(*owner), deposits, borrows, + deposited_value: unpack_decimal(deposited_value), + borrowed_value: unpack_decimal(borrowed_value), + loan_to_value_ratio: unpack_rate(loan_to_value_ratio), + liquidation_threshold: unpack_rate(liquidation_threshold), }) } } From b8b0794d27a61e8ebc159916b394b68c95ed990d Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:22:14 -0500 Subject: [PATCH 148/191] fmt / clippy fixes --- token-lending/program/src/processor.rs | 16 ++-- .../program/src/state/lending_market.rs | 3 +- token-lending/program/src/state/obligation.rs | 3 +- token-lending/program/src/state/reserve.rs | 79 +++++++++++-------- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 15074c86e14..18f29c9bb83 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -774,17 +774,17 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidSigner.into()); } - // @TODO: does there need to be a check to make sure obligation_token_mint_info is rent exempt? + // @TODO: Does there need to be a check to make sure obligation_token_mint_info is rent exempt? let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint.is_initialized() { if obligation_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } - if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { + if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) + { return Err(LendingError::InvalidMarketAuthority.into()); } - } - else { + } else { spl_token_init_mint(TokenInitializeMintParams { mint: obligation_token_mint_info.clone(), authority: lending_market_authority_info.key, @@ -794,8 +794,9 @@ fn process_deposit_obligation_collateral( })?; } - // @TODO: does there need to be a check to make sure obligation_token_output_info is rent exempt? - let obligation_token_output = Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; + // @TODO: Does there need to be a check to make sure obligation_token_output_info is rent exempt? + let obligation_token_output = + Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; if obligation_token_output.is_initialized() { if obligation_token_output_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); @@ -806,8 +807,7 @@ fn process_deposit_obligation_collateral( if &obligation_token_output.owner != obligation_owner_info.key { return Err(LendingError::InvalidObligationOwner.into()); } - } - else { + } else { spl_token_init_account(TokenInitializeAccountParams { account: obligation_token_output_info.clone(), mint: obligation_token_mint_info.clone(), diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 48ef765137b..52a95a63bfe 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -58,7 +58,8 @@ impl IsInitialized for LendingMarket { } } -// @TODO: adjust padding. what's a reasonable number? +// @TODO: Adjust padding, but what's a reasonable number? +// Or should there be no padding to save space, but we need account resizing implemented? const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128 impl Pack for LendingMarket { const LEN: usize = LENDING_MARKET_LEN; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index b8e5823c3fa..77bb8890e7d 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -325,7 +325,8 @@ impl ObligationLiquidity { } } -// @TODO: adjust padding. what's a reasonable number? +// @TODO: Adjust padding, but what's a reasonable number? +// Or should there be no padding to save space, but we need account resizing implemented? const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 932; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 8 + 8 + 1 + 1 + (88 * 1) + (80 * 9) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b3d64cd91ae..30a5ab0fd1d 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -12,6 +12,7 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; +use std::cmp::Ordering; use std::convert::{TryFrom, TryInto}; /// Percentage of an obligation that can be repaid during each liquidation call @@ -157,8 +158,8 @@ impl Reserve { .ok_or(LendingError::MathOverflow)?; if liquidity_amount == u64::max_value() { let borrow_amount = max_borrow_value - .try_div(self.liquidity.median_price)? .try_mul(decimals)? + .try_div(self.liquidity.median_price)? .min(self.liquidity.available_amount.into()); let (origination_fee, host_fee) = self .config @@ -185,8 +186,8 @@ impl Reserve { let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; let borrow_value = borrow_amount - .try_div(decimals)? - .try_mul(self.liquidity.median_price)?; + .try_mul(self.liquidity.median_price)? + .try_div(decimals)?; if borrow_value > max_borrow_value { return Err(LendingError::BorrowTooLarge.into()); } @@ -249,19 +250,23 @@ impl Reserve { settle_amount = liquidity.borrowed_amount_wads; let liquidation_value = liquidity.market_value.try_mul(bonus_rate)?; - if liquidation_value > collateral.market_value { - let repay_pct = collateral.market_value.try_div(liquidation_value)?; - repay_amount = target_amount.try_mul(repay_pct)?.try_ceil_u64()?; - withdraw_amount = collateral.deposited_amount; - } else if liquidation_value == collateral.market_value { - repay_amount = target_amount.try_ceil_u64()?; - withdraw_amount = collateral.deposited_amount; - } else { - let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; - repay_amount = target_amount.try_ceil_u64()?; - withdraw_amount = Decimal::from(collateral.deposited_amount) - .try_mul(withdraw_pct)? - .try_ceil_u64()?; + match liquidation_value.cmp(&collateral.market_value) { + Ordering::Greater => { + let repay_pct = collateral.market_value.try_div(liquidation_value)?; + repay_amount = target_amount.try_mul(repay_pct)?.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Equal => { + repay_amount = target_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Less => { + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + repay_amount = target_amount.try_ceil_u64()?; + withdraw_amount = Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_ceil_u64()?; + } } } else { // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded up @@ -274,19 +279,23 @@ impl Reserve { .try_mul(liquidation_pct)? .try_mul(bonus_rate)?; - if liquidation_value > collateral.market_value { - let repay_pct = collateral.market_value.try_div(liquidation_value)?; - settle_amount = liquidation_amount.try_mul(repay_pct)?; - withdraw_amount = collateral.deposited_amount; - } else if liquidation_value == collateral.market_value { - settle_amount = liquidation_amount; - withdraw_amount = collateral.deposited_amount; - } else { - let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; - settle_amount = liquidation_amount; - withdraw_amount = Decimal::from(collateral.deposited_amount) - .try_mul(withdraw_pct)? - .try_ceil_u64()?; + 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)?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Equal => { + settle_amount = liquidation_amount; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Less => { + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + settle_amount = liquidation_amount; + withdraw_amount = Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_ceil_u64()?; + } } repay_amount = settle_amount.try_ceil_u64()?; @@ -577,7 +586,7 @@ impl From for Rate { /// Reserve configuration values #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct ReserveConfig { - /// Optimal utilization rate as a percent + /// Optimal utilization rate, as a percentage pub optimal_utilization_rate: u8, /// Min borrow APY pub min_borrow_rate: u8, @@ -585,11 +594,12 @@ pub struct ReserveConfig { pub optimal_borrow_rate: u8, /// Max borrow APY pub max_borrow_rate: u8, - /// Ratio of the value of borrows to deposits as a percent; 0 if use as collateral is disabled + /// 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 percent at which an obligation is considered unhealthy + /// Loan to value ratio at which an obligation can be liquidated, as a percentage pub liquidation_threshold: u8, - /// The percent bonus the liquidator gets when repaying liquidity to an unhealthy obligation + /// Bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage pub liquidation_bonus: u8, /// Program owner fees assessed, separate from gains due to interest accrual pub fees: ReserveFees, @@ -673,7 +683,8 @@ impl IsInitialized for Reserve { } } -// @TODO: adjust padding. what's a reasonable number? +// @TODO: Adjust padding, but what's a reasonable number? +// Or should there be no padding to save space, but we need account resizing implemented? const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; From d9da80768ff4404911da171dfa63e2d184fc28d9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:35:51 -0500 Subject: [PATCH 149/191] median_price -> market_price --- token-lending/program/src/processor.rs | 10 +++++----- token-lending/program/src/state/reserve.rs | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 18f29c9bb83..41811ac1f44 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -241,7 +241,7 @@ fn process_init_reserve( return Err(LendingError::InvalidSigner.into()); } - let (reserve_liquidity_aggregator, reserve_liquidity_median_price) = + let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { if account_info_iter.peek().is_some() { msg!("Invalid reserve liquidity aggregator account"); @@ -283,7 +283,7 @@ fn process_init_reserve( supply_pubkey: *reserve_liquidity_supply_info.key, fee_receiver: *reserve_liquidity_fee_receiver_info.key, aggregator: reserve_liquidity_aggregator, - median_price: reserve_liquidity_median_price, + market_price: reserve_liquidity_market_price, }), collateral: ReserveCollateral::new(NewReserveCollateralParams { mint_pubkey: *reserve_collateral_mint_info.key, @@ -365,7 +365,7 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidAccountInput.into()); } - reserve.liquidity.median_price = read_median(reserve_liquidity_aggregator_info)?.median; + reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; } else if account_info_iter.peek().is_some() { msg!("Invalid reserve liquidity aggregator account"); return Err(LendingError::InvalidAccountInput.into()); @@ -636,7 +636,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let market_value = deposit_reserve .collateral_exchange_rate()? .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? - .try_mul(deposit_reserve.liquidity.median_price)? + .try_mul(deposit_reserve.liquidity.market_price)? .try_div(decimals)?; collateral.market_value = market_value; @@ -673,7 +673,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; let market_value = liquidity .borrowed_amount_wads - .try_mul(borrow_reserve.liquidity.median_price)? + .try_mul(borrow_reserve.liquidity.market_price)? .try_div(decimals)?; liquidity.market_value = market_value; borrowed_value = borrowed_value.try_add(market_value)?; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 30a5ab0fd1d..860b68a5187 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -159,7 +159,7 @@ impl Reserve { if liquidity_amount == u64::max_value() { let borrow_amount = max_borrow_value .try_mul(decimals)? - .try_div(self.liquidity.median_price)? + .try_div(self.liquidity.market_price)? .min(self.liquidity.available_amount.into()); let (origination_fee, host_fee) = self .config @@ -186,7 +186,7 @@ impl Reserve { let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; let borrow_value = borrow_amount - .try_mul(self.liquidity.median_price)? + .try_mul(self.liquidity.market_price)? .try_div(decimals)?; if borrow_value > max_borrow_value { return Err(LendingError::BorrowTooLarge.into()); @@ -358,8 +358,8 @@ pub struct ReserveLiquidity { pub aggregator: COption, /// Reserve liquidity cumulative borrow rate pub cumulative_borrow_rate_wads: Decimal, - /// Reserve liquidity median price in quote currency - pub median_price: u64, + /// Reserve liquidity market price in quote currency + pub market_price: u64, /// Reserve liquidity available pub available_amount: u64, /// Reserve liquidity borrowed @@ -378,8 +378,8 @@ pub struct NewReserveLiquidityParams { pub fee_receiver: Pubkey, /// Optional reserve liquidity aggregator state account pub aggregator: COption, - /// Reserve liquidity median price in quote currency - pub median_price: u64, + /// Reserve liquidity market price in quote currency + pub market_price: u64, } impl ReserveLiquidity { @@ -392,7 +392,7 @@ impl ReserveLiquidity { fee_receiver: params.fee_receiver, aggregator: params.aggregator, cumulative_borrow_rate_wads: Decimal::one(), - median_price: params.median_price, + market_price: params.market_price, available_amount: 0, borrowed_amount_wads: Decimal::zero(), } @@ -703,7 +703,7 @@ impl Pack for Reserve { liquidity_fee_receiver, liquidity_aggregator, liquidity_cumulative_borrow_rate_wads, - liquidity_median_price, + liquidity_market_price, liquidity_available_amount, liquidity_borrowed_amount_wads, collateral_mint, @@ -763,7 +763,7 @@ impl Pack for Reserve { self.liquidity.cumulative_borrow_rate_wads, liquidity_cumulative_borrow_rate_wads, ); - *liquidity_median_price = self.liquidity.median_price.to_le_bytes(); + *liquidity_market_price = self.liquidity.market_price.to_le_bytes(); *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); pack_decimal( self.liquidity.borrowed_amount_wads, @@ -802,7 +802,7 @@ impl Pack for Reserve { liquidity_fee_receiver, liquidity_aggregator, liquidity_cumulative_borrow_rate_wads, - liquidity_median_price, + liquidity_market_price, liquidity_available_amount, liquidity_borrowed_amount_wads, collateral_mint, @@ -861,7 +861,7 @@ impl Pack for Reserve { fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), aggregator: unpack_coption_key(liquidity_aggregator)?, cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), - median_price: u64::from_le_bytes(*liquidity_median_price), + market_price: u64::from_le_bytes(*liquidity_market_price), available_amount: u64::from_le_bytes(*liquidity_available_amount), borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), }, From 28b5b18bd9ee515f4d6f432fdcc5b0f0807f7606 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:43:02 -0500 Subject: [PATCH 150/191] reserves are already checked to be not stale --- token-lending/program/src/processor.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 41811ac1f44..6069e8f0768 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -919,9 +919,6 @@ fn process_withdraw_obligation_collateral( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update < withdraw_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; @@ -1090,9 +1087,6 @@ fn process_borrow_obligation_liquidity( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update < borrow_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } if &obligation.owner != obligation_owner_info.key { return Err(LendingError::InvalidObligationOwner.into()); } @@ -1241,9 +1235,6 @@ fn process_repay_obligation_liquidity( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update < repay_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; @@ -1363,12 +1354,6 @@ fn process_liquidate_obligation( if obligation.last_update.is_stale(clock.slot)? { return Err(LendingError::ObligationStale.into()); } - if obligation.last_update < repay_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } - if obligation.last_update < withdraw_reserve.last_update { - return Err(LendingError::ObligationStale.into()); - } if obligation.deposited_value == Decimal::zero() { return Err(LendingError::ObligationDepositsZero.into()); } From 68509f20891f500309254909b5c6fd496741662c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 13:44:04 -0500 Subject: [PATCH 151/191] check that obligation token mint authority matches lending market --- token-lending/program/src/processor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6069e8f0768..f079324eda5 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -933,6 +933,9 @@ fn process_withdraw_obligation_collateral( if obligation_token_mint_info.owner != token_program_id.key { return Err(LendingError::InvalidTokenOwner.into()); } + if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { + return Err(LendingError::InvalidMarketAuthority.into()); + } let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; if obligation_token_input_info.owner != token_program_id.key { From 6e42c822341167d890d1b510d15dbb762675a81f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 18:08:46 -0500 Subject: [PATCH 152/191] add more checks and revisit all errors --- token-lending/program/src/instruction.rs | 58 ++-- token-lending/program/src/processor.rs | 288 ++++++++++++++---- .../program/src/state/lending_market.rs | 2 + token-lending/program/src/state/mod.rs | 15 +- token-lending/program/src/state/obligation.rs | 48 ++- token-lending/program/src/state/reserve.rs | 21 +- 6 files changed, 321 insertions(+), 111 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 9cfda27aca4..4ac8a77568e 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -6,6 +6,7 @@ use crate::{ }; use solana_program::{ instruction::{AccountMeta, Instruction}, + msg, program_error::ProgramError, pubkey::{Pubkey, PUBKEY_BYTES}, sysvar, @@ -347,46 +348,49 @@ impl LendingInstruction { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::LiquidateObligation { liquidity_amount } } - _ => return Err(LendingError::InstructionUnpackError.into()), + _ => { + msg!("Instruction cannot be unpacked"); + return Err(LendingError::InstructionUnpackError.into()); + } }) } fn unpack_u64(input: &[u8]) -> Result<(u64, &[u8]), ProgramError> { - if input.len() >= 8 { - let (amount, rest) = input.split_at(8); - let amount = amount - .get(..8) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(LendingError::InstructionUnpackError)?; - Ok((amount, rest)) - } else { - Err(LendingError::InstructionUnpackError.into()) + if input.len() < 8 { + msg!("u64 cannot be unpacked"); + return Err(LendingError::InstructionUnpackError.into()); } + let (amount, rest) = input.split_at(8); + let amount = amount + .get(..8) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(LendingError::InstructionUnpackError)?; + Ok((amount, rest)) } fn unpack_u8(input: &[u8]) -> Result<(u8, &[u8]), ProgramError> { - if !input.is_empty() { - let (amount, rest) = input.split_at(1); - let amount = amount - .get(..1) - .and_then(|slice| slice.try_into().ok()) - .map(u8::from_le_bytes) - .ok_or(LendingError::InstructionUnpackError)?; - Ok((amount, rest)) - } else { - Err(LendingError::InstructionUnpackError.into()) + if input.is_empty() { + msg!("u8 cannot be unpacked"); + return Err(LendingError::InstructionUnpackError.into()); } + let (amount, rest) = input.split_at(1); + let amount = amount + .get(..1) + .and_then(|slice| slice.try_into().ok()) + .map(u8::from_le_bytes) + .ok_or(LendingError::InstructionUnpackError)?; + Ok((amount, rest)) } fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { - if input.len() >= PUBKEY_BYTES { - let (key, rest) = input.split_at(PUBKEY_BYTES); - let pk = Pubkey::new(key); - Ok((pk, rest)) - } else { - Err(LendingError::InstructionUnpackError.into()) + if input.len() < PUBKEY_BYTES { + msg!("Pubkey cannot be unpacked"); + return Err(LendingError::InstructionUnpackError.into()); } + let (key, rest) = input.split_at(PUBKEY_BYTES); + let pk = Pubkey::new(key); + Ok((pk, rest)) } /// Packs a [LendingInstruction](enum.LendingInstruction.html) into a byte buffer. diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f079324eda5..e1226cb707d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -107,9 +107,14 @@ fn process_init_lending_market( assert_rent_exempt(rent, lending_market_info)?; let mut lending_market = assert_uninitialized::(lending_market_info)?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } unpack_mint("e_token_mint_info.data.borrow())?; if quote_token_mint_info.owner != token_program_id.key { + msg!("Quote token mint provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } @@ -136,12 +141,15 @@ fn process_set_lending_market_owner( let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); return Err(LendingError::InvalidMarketOwner.into()); } if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } @@ -216,6 +224,11 @@ fn process_init_reserve( assert_rent_exempt(rent, reserve_info)?; let mut reserve = assert_uninitialized::(reserve_info)?; + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + assert_uninitialized::(reserve_liquidity_supply_info)?; assert_uninitialized::(reserve_liquidity_fee_receiver_info)?; assert_uninitialized::(reserve_collateral_mint_info)?; @@ -223,28 +236,32 @@ fn process_init_reserve( assert_uninitialized::(destination_collateral_info)?; if reserve_liquidity_supply_info.key == source_liquidity_info.key { - msg!("Invalid source liquidity account"); + msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); return Err(LendingError::InvalidMarketOwner.into()); } if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { if account_info_iter.peek().is_some() { - msg!("Invalid reserve liquidity aggregator account"); + msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); return Err(LendingError::InvalidAccountInput.into()); } // 1 because quote token price is equal to itself @@ -264,12 +281,16 @@ fn process_init_reserve( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } let reserve_liquidity_mint = unpack_mint(&reserve_liquidity_mint_info.data.borrow())?; if reserve_liquidity_mint_info.owner != token_program_id.key { + msg!("Reserve liquidity mint is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } @@ -355,19 +376,22 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { - msg!("Invalid reserve liquidity aggregator account"); + msg!( + "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" + ); return Err(LendingError::InvalidAccountInput.into()); } reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; } else if account_info_iter.peek().is_some() { - msg!("Invalid reserve liquidity aggregator account"); + msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); return Err(LendingError::InvalidAccountInput.into()); } @@ -384,6 +408,7 @@ fn process_deposit_reserve_liquidity( accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -401,37 +426,41 @@ fn process_deposit_reserve_liquidity( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey != reserve_liquidity_supply_info.key { - msg!("Invalid reserve liquidity supply account"); + msg!("Reserve liquidity supply does not match the reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.mint_pubkey != reserve_collateral_mint_info.key { - msg!("Invalid reserve collateral mint account"); + msg!("Reserve collateral mint does not match the reserve collateral mint provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Invalid source liquidity account"); + msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Invalid destination collateral account"); + msg!("Reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if reserve.last_update.is_stale(clock.slot)? { + msg!("Reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } @@ -441,7 +470,10 @@ fn process_deposit_reserve_liquidity( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } @@ -476,6 +508,7 @@ fn process_redeem_reserve_collateral( accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -493,37 +526,41 @@ fn process_redeem_reserve_collateral( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &reserve.liquidity.supply_pubkey != reserve_liquidity_supply_info.key { - msg!("Invalid reserve liquidity supply account"); + if &reserve.collateral.mint_pubkey != reserve_collateral_mint_info.key { + msg!("Reserve collateral mint does not match the reserve collateral mint provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &reserve.collateral.mint_pubkey != reserve_collateral_mint_info.key { - msg!("Invalid reserve collateral mint account"); + if &reserve.collateral.supply_pubkey == source_collateral_info.key { + msg!("Reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Invalid destination liquidity account"); + if &reserve.liquidity.supply_pubkey != reserve_liquidity_supply_info.key { + msg!("Reserve liquidity supply does not match the reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Invalid source collateral account"); + if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + msg!("Reserve liquidity supply cannot be used as the destination liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if reserve.last_update.is_stale(clock.slot)? { + msg!("Reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } @@ -533,7 +570,10 @@ fn process_redeem_reserve_collateral( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } @@ -574,16 +614,23 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro assert_rent_exempt(rent, obligation_info)?; let mut obligation = assert_uninitialized::(obligation_info)?; + if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } @@ -606,6 +653,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } @@ -614,18 +662,29 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut loan_to_value_ratio = Decimal::zero(); let mut liquidation_threshold = Decimal::zero(); - for collateral in &mut obligation.deposits { + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { + msg!( + "Deposit reserve provided for collateral {} is not owned by the lending program", + index + ); return Err(LendingError::InvalidAccountOwner.into()); } if collateral.deposit_reserve != *deposit_reserve_info.key { - msg!("Invalid deposit reserve"); + msg!( + "Deposit reserve of collateral {} does not match the deposit reserve provided", + index + ); return Err(LendingError::InvalidAccountInput.into()); } let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", + index + ); return Err(LendingError::ReserveStale.into()); } @@ -651,18 +710,29 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } - for liquidity in &mut obligation.borrows { + for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { + msg!( + "Borrow reserve provided for liquidity {} is not owned by the lending program", + index + ); return Err(LendingError::InvalidAccountOwner.into()); } if liquidity.borrow_reserve != *borrow_reserve_info.key { - msg!("Invalid borrow reserve"); + msg!( + "Borrow reserve of liquidity {} does not match the borrow reserve provided", + index + ); return Err(LendingError::InvalidAccountInput.into()); } let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", + index + ); return Err(LendingError::ReserveStale.into()); } @@ -680,7 +750,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> } if account_info_iter.peek().is_some() { - msg!("Too many obligation collateral or liquidity accounts"); + msg!("Too many obligation deposit or borrow reserves provided"); return Err(LendingError::InvalidAccountInput.into()); } @@ -710,6 +780,7 @@ fn process_deposit_obligation_collateral( accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -730,47 +801,57 @@ fn process_deposit_obligation_collateral( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; if deposit_reserve_info.owner != program_id { + msg!("Deposit reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Deposit reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { - msg!("Invalid destination collateral account"); + if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { + msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Invalid source collateral account"); + if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { + msg!( + "Deposit reserve collateral supply must be used as the destination collateral provided" + ); return Err(LendingError::InvalidAccountInput.into()); } if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!("Deposit reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } if deposit_reserve.config.loan_to_value_ratio == 0 { + msg!("Deposit reserve has collateral disabled for borrowing"); return Err(LendingError::ReserveCollateralDisabled.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); + msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } @@ -778,11 +859,13 @@ fn process_deposit_obligation_collateral( let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint.is_initialized() { if obligation_token_mint_info.owner != token_program_id.key { + msg!("Obligation token mint provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { - return Err(LendingError::InvalidMarketAuthority.into()); + msg!("Obligation token mint authority does not match the lending market authority provided"); + return Err(LendingError::InvalidAccountInput.into()); } } else { spl_token_init_mint(TokenInitializeMintParams { @@ -799,12 +882,15 @@ fn process_deposit_obligation_collateral( Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; if obligation_token_output.is_initialized() { if obligation_token_output_info.owner != token_program_id.key { + msg!("Obligation token output provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } if &obligation_token_output.mint != obligation_token_mint_info.key { + msg!("Obligation token output mint does not match the obligation token mint provided"); return Err(LendingError::InvalidTokenMint.into()); } if &obligation_token_output.owner != obligation_owner_info.key { + msg!("Obligation token output owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } } else { @@ -823,7 +909,10 @@ fn process_deposit_obligation_collateral( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } @@ -864,6 +953,7 @@ fn process_withdraw_obligation_collateral( accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -882,66 +972,80 @@ fn process_withdraw_obligation_collateral( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { + msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &withdraw_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { - msg!("Invalid source collateral account"); + msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Invalid destination collateral account"); + msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if withdraw_reserve.last_update.is_stale(clock.slot)? { + msg!("Withdraw reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); + msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; if &collateral.token_mint != obligation_token_mint_info.key { + msg!("Collateral token mint does not match the obligation token mint provided"); return Err(LendingError::InvalidTokenMint.into()); } if collateral.deposited_amount == 0 { + msg!("Collateral deposited amount is zero"); return Err(LendingError::ObligationCollateralEmpty.into()); } let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; if obligation_token_mint_info.owner != token_program_id.key { + msg!("Obligation token mint provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { - return Err(LendingError::InvalidMarketAuthority.into()); + msg!( + "Obligation token mint authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidAccountOwner.into()); } let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; if obligation_token_input_info.owner != token_program_id.key { + msg!("Obligation token input provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } if &obligation_token_input.mint != obligation_token_mint_info.key { + msg!("Obligation token input mint does not match the obligation token mint provided"); return Err(LendingError::InvalidTokenMint.into()); } @@ -951,7 +1055,10 @@ fn process_withdraw_obligation_collateral( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } @@ -960,14 +1067,17 @@ fn process_withdraw_obligation_collateral( collateral.deposited_amount } else if obligation.borrowed_value == Decimal::zero() { // there are borrows, but they cannot be valued; they must be repaid to withdraw collateral + msg!("Obligation borrowed value is zero"); return Err(LendingError::ObligationBorrowsZero.into()); } else if obligation.deposited_value == Decimal::zero() { // there are deposits, but they cannot be valued + msg!("Obligation deposited value is zero"); return Err(LendingError::ObligationDepositsZero.into()); } else { // there are borrows and deposits, and they can both be valued let max_withdraw_value = obligation.max_withdraw_value()?; if max_withdraw_value == Decimal::zero() { + msg!("Maximum withdraw value is zero"); return Err(LendingError::WithdrawTooLarge.into()); } @@ -984,11 +1094,13 @@ fn process_withdraw_obligation_collateral( Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { + msg!("Withdraw value cannot exceed maximum withdraw value"); return Err(LendingError::WithdrawTooLarge.into()); } withdraw_amount }; if withdraw_amount == 0 { + msg!("Withdraw amount is too small to transfer collateral"); return Err(LendingError::WithdrawTooSmall.into()); } withdraw_amount @@ -997,6 +1109,7 @@ fn process_withdraw_obligation_collateral( let obligation_token_amount = collateral .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; if obligation_token_amount == 0 { + msg!("Withdraw amount is too small to burn obligation tokens"); return Err(LendingError::WithdrawTooSmall.into()); } @@ -1032,6 +1145,7 @@ fn process_borrow_obligation_liquidity( accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -1049,57 +1163,69 @@ fn process_borrow_obligation_liquidity( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve_info.owner != program_id { + msg!("Borrow reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Borrow reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Invalid source liquidity account"); + msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!("Invalid destination liquidity account"); + msg!( + "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" + ); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { - msg!("Invalid borrow reserve liquidity fee receiver account"); + msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); return Err(LendingError::InvalidAccountInput.into()); } if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!("Borrow reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); + msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } if obligation.deposits.is_empty() { + msg!("Obligation has no deposits to borrow against"); return Err(LendingError::ObligationDepositsEmpty.into()); } if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposits have zero value"); return Err(LendingError::ObligationDepositsZero.into()); } @@ -1109,12 +1235,16 @@ fn process_borrow_obligation_liquidity( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } let max_borrow_value = obligation.max_borrow_value()?; if max_borrow_value == Decimal::zero() { + msg!("Maximum borrow value is zero"); return Err(LendingError::BorrowTooLarge.into()); } @@ -1126,6 +1256,7 @@ fn process_borrow_obligation_liquidity( } = borrow_reserve.borrow_liquidity(liquidity_amount, max_borrow_value)?; if receive_amount == 0 { + msg!("Borrow amount is too small to receive liquidity after fees"); return Err(LendingError::BorrowTooSmall.into()); } @@ -1186,6 +1317,7 @@ fn process_repay_obligation_liquidity( accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -1201,47 +1333,54 @@ fn process_repay_obligation_liquidity( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &repay_reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); + msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Invalid source liquidity account"); + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { - msg!("Invalid destination liquidity account"); + msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); + msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; if liquidity.borrowed_amount_wads == Decimal::zero() { + msg!("Liquidity borrowed amount is zero"); return Err(LendingError::ObligationLiquidityEmpty.into()); } @@ -1251,6 +1390,7 @@ fn process_repay_obligation_liquidity( } = repay_reserve.repay_liquidity(liquidity_amount, liquidity.borrowed_amount_wads)?; if repay_amount == 0 { + msg!("Repay amount is too small to transfer liquidity"); return Err(LendingError::RepayTooSmall.into()); } @@ -1281,6 +1421,7 @@ fn process_liquidate_obligation( accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } @@ -1300,70 +1441,91 @@ fn process_liquidate_obligation( let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &repay_reserve.lending_market != lending_market_info.key { - msg!("Invalid repay reserve lending market account"); + msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Invalid repay reserve liquidity supply account"); + msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Invalid source liquidity account"); + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!( + "Repay reserve collateral supply cannot be used as the destination collateral provided" + ); return Err(LendingError::InvalidAccountInput.into()); } if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { + msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &withdraw_reserve.lending_market != lending_market_info.key { - msg!("Invalid withdraw reserve lending market account"); + msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Invalid withdraw reserve collateral supply account"); + msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Invalid destination collateral account"); + msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if withdraw_reserve.last_update.is_stale(clock.slot)? { + msg!("Withdraw reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { - msg!("Invalid obligation lending market account"); + msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposited value is zero"); return Err(LendingError::ObligationDepositsZero.into()); } if obligation.borrowed_value == Decimal::zero() { + msg!("Obligation borrowed value is zero"); return Err(LendingError::ObligationBorrowsZero.into()); } if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { + msg!("Obligation is healthy and cannot be liquidated"); return Err(LendingError::ObligationHealthy.into()); } @@ -1378,7 +1540,10 @@ fn process_liquidate_obligation( ]; let lending_market_authority_pubkey = Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if lending_market_authority_info.key != &lending_market_authority_pubkey { + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); return Err(LendingError::InvalidMarketAuthority.into()); } @@ -1393,7 +1558,12 @@ fn process_liquidate_obligation( &collateral, )?; - if repay_amount == 0 || withdraw_amount == 0 { + if repay_amount == 0 { + msg!("Liquidation is too small to transfer liquidity"); + return Err(LendingError::LiquidationTooSmall.into()); + } + if withdraw_amount == 0 { + msg!("Liquidation is too small to receive collateral"); return Err(LendingError::LiquidationTooSmall.into()); } diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 52a95a63bfe..5de3807ff7e 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -1,6 +1,7 @@ use super::*; 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}, @@ -84,6 +85,7 @@ impl Pack for LendingMarket { array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; 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); } diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 006718aad81..3461dcd892f 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -14,6 +14,7 @@ use crate::math::{Decimal, Rate, WAD}; use arrayref::{array_refs, mut_array_refs}; use solana_program::{ clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, + msg, program_error::ProgramError, program_option::COption, pubkey::{Pubkey, PUBKEY_BYTES}, @@ -55,14 +56,17 @@ fn unpack_coption_key(src: &[u8; 4 + PUBKEY_BYTES]) -> Result, P match *tag { [0, 0, 0, 0] => Ok(COption::None), [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), - _ => Err(ProgramError::InvalidAccountData), + _ => { + msg!("COption cannot be unpacked"); + Err(ProgramError::InvalidAccountData) + } } } fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { *dst = decimal .to_scaled_val() - .expect("could not pack decimal") + .expect("Decimal cannot be packed") .to_le_bytes(); } @@ -75,7 +79,7 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { fn pack_rate(rate: Rate, dst: &mut [u8; 8]) { *dst = rate .try_round_u64() - .expect("could not pack rate") + .expect("Rate cannot be packed") .to_le_bytes(); } @@ -91,7 +95,10 @@ fn unpack_bool(src: &[u8; 1]) -> Result { match u8::from_le_bytes(*src) { 0 => Ok(false), 1 => Ok(true), - _ => Err(ProgramError::InvalidAccountData), + _ => { + msg!("Boolean cannot be unpacked"); + Err(ProgramError::InvalidAccountData) + } } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 77bb8890e7d..6480b7006b3 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -7,6 +7,7 @@ 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}, @@ -137,6 +138,7 @@ impl Obligation { 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 @@ -153,16 +155,21 @@ impl Obligation { ) -> Result<&mut ObligationCollateral, ProgramError> { if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) { if self.deposits[collateral_index].token_mint != token_mint { + msg!("Token mint of obligation collateral {} does not match the obligation token mint provided", collateral_index); return Err(LendingError::InvalidTokenMint.into()); } - Ok(&mut self.deposits[collateral_index]) - } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { - Err(LendingError::ObligationReserveLimit.into()) - } else { - let collateral = ObligationCollateral::new(deposit_reserve, token_mint); - self.deposits.push(collateral); - Ok(self.deposits.last_mut().unwrap()) + 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, token_mint); + self.deposits.push(collateral); + Ok(self.deposits.last_mut().unwrap()) } fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option { @@ -177,6 +184,7 @@ impl Obligation { 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 @@ -191,14 +199,18 @@ impl Obligation { borrow_reserve: Pubkey, ) -> Result<&mut ObligationLiquidity, ProgramError> { if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) { - Ok(&mut self.borrows[liquidity_index]) - } else if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { - Err(LendingError::ObligationReserveLimit.into()) - } else { - let liquidity = ObligationLiquidity::new(borrow_reserve); - self.borrows.push(liquidity); - Ok(self.borrows.last_mut().unwrap()) + 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); + self.borrows.push(liquidity); + Ok(self.borrows.last_mut().unwrap()) } fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option { @@ -309,6 +321,7 @@ impl ObligationLiquidity { /// Accrue interest pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { + msg!("Interest rate cannot be negative"); return Err(LendingError::NegativeInterestRate.into()); } @@ -436,6 +449,11 @@ impl Pack for Obligation { 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); @@ -471,7 +489,7 @@ impl Pack for Obligation { } Ok(Self { - version: u8::from_le_bytes(*version), + version, last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), stale: unpack_bool(last_update_stale)?, diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 860b68a5187..f30fed4ddae 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -7,6 +7,7 @@ use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ clock::Slot, entrypoint::ProgramResult, + msg, program_error::ProgramError, program_option::COption, program_pack::{IsInitialized, Pack, Sealed}, @@ -189,6 +190,7 @@ impl Reserve { .try_mul(self.liquidity.market_price)? .try_div(decimals)?; if borrow_value > max_borrow_value { + msg!("Borrow value cannot exceed maximum borrow value"); return Err(LendingError::BorrowTooLarge.into()); } @@ -415,6 +417,7 @@ impl ReserveLiquidity { /// 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 @@ -428,6 +431,7 @@ impl ReserveLiquidity { pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { let receive_amount = borrow_amount.try_floor_u64()?; if receive_amount > self.available_amount { + msg!("Borrow amount cannot exceed available amount"); return Err(LendingError::InsufficientLiquidity.into()); } @@ -658,6 +662,10 @@ impl ReserveFees { }; let borrow_fee = borrow_fee_amount.try_round_u64()?.max(minimum_fee); + if Decimal::from(borrow_fee) >= borrow_amount { + msg!("Borrow amount is too small to receive liquidity after fees"); + return Err(LendingError::BorrowTooSmall.into()); + } let host_fee = if need_to_assess_host_fee { host_fee_rate.try_mul(borrow_fee)?.try_round_u64()?.max(1) @@ -665,11 +673,7 @@ impl ReserveFees { 0 }; - if Decimal::from(borrow_fee) >= borrow_amount { - Err(LendingError::BorrowTooSmall.into()) - } else { - Ok((borrow_fee, host_fee)) - } + Ok((borrow_fee, host_fee)) } else { Ok((0, 0)) } @@ -847,8 +851,13 @@ impl Pack for Reserve { 1, 256 ]; + let version = u8::from_le_bytes(*version); + if version > PROGRAM_VERSION { + msg!("Reserve version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } Ok(Self { - version: u8::from_le_bytes(*version), + version, last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), stale: unpack_bool(last_update_stale)?, From 17ea0b82a5b4ef955787dab0766532346c875726 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 18:57:32 -0500 Subject: [PATCH 153/191] can't fit Rate in 8 bytes without losing precision --- token-lending/program/src/state/mod.rs | 15 +++++---------- token-lending/program/src/state/obligation.rs | 10 +++++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 3461dcd892f..068f4823e17 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -10,7 +10,7 @@ pub use lending_market::*; pub use obligation::*; pub use reserve::*; -use crate::math::{Decimal, Rate, WAD}; +use crate::math::{Decimal, Rate, U128, WAD}; use arrayref::{array_refs, mut_array_refs}; use solana_program::{ clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, @@ -74,17 +74,12 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { Decimal::from_scaled_val(u128::from_le_bytes(*src)) } -// @TODO: Rate::to_scaled_val doesn't work here because it requires 16 bytes. -// Is it a problem to round to u64? -fn pack_rate(rate: Rate, dst: &mut [u8; 8]) { - *dst = rate - .try_round_u64() - .expect("Rate cannot be packed") - .to_le_bytes(); +fn pack_rate(rate: Rate, dst: &mut [u8; 16]) { + *dst = rate.to_scaled_val().to_le_bytes(); } -fn unpack_rate(src: &[u8; 8]) -> Rate { - Rate::from_scaled_val(u64::from_le_bytes(*src)) +fn unpack_rate(src: &[u8; 16]) -> Rate { + Rate(U128::from(u128::from_le_bytes(*src))) } fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 6480b7006b3..a899e63c3ce 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -342,7 +342,7 @@ impl ObligationLiquidity { // Or should there be no padding to save space, but we need account resizing implemented? const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 -const OBLIGATION_LEN: usize = 932; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 8 + 8 + 1 + 1 + (88 * 1) + (80 * 9) +const OBLIGATION_LEN: usize = 948; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (88 * 1) + (80 * 9) impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -371,8 +371,8 @@ impl Pack for Obligation { PUBKEY_BYTES, 16, 16, - 8, - 8, + 16, + 16, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -443,8 +443,8 @@ impl Pack for Obligation { PUBKEY_BYTES, 16, 16, - 8, - 8, + 16, + 16, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) From bc7413413b41530bf870732e0abeb10753e4f3c5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 6 Apr 2021 15:57:52 -0500 Subject: [PATCH 154/191] rewrite functional tests --- token-lending/program/src/state/mod.rs | 3 +- .../program/tests/accrue_interest.rs | 108 --- token-lending/program/tests/borrow.rs | 325 ------- .../tests/borrow_obligation_liquidity.rs | 401 ++++++++ .../tests/deposit_obligation_collateral.rs | 127 ++- ...eposit.rs => deposit_reserve_liquidity.rs} | 11 +- .../program/tests/fixtures/README.md | 11 + .../tests/fixtures/btc_usd_aggregator.bin | Bin 0 -> 229 bytes .../tests/fixtures/eth_usd_aggregator.bin | Bin 0 -> 229 bytes .../program/tests/genesis_accounts.rs | 614 ++++++------ token-lending/program/tests/helpers/mod.rs | 870 ++++++++++-------- .../program/tests/init_lending_market.rs | 16 +- .../program/tests/init_obligation.rs | 79 +- token-lending/program/tests/init_reserve.rs | 48 +- token-lending/program/tests/liquidate.rs | 162 ---- .../program/tests/liquidate_obligation.rs | 182 ++++ ...thdraw.rs => redeem_reserve_collateral.rs} | 39 +- .../program/tests/refresh_obligation.rs | 164 ++++ .../program/tests/refresh_reserve.rs | 132 +++ token-lending/program/tests/repay.rs | 191 ---- .../tests/repay_obligation_liquidity.rs | 145 +++ .../program/tests/set_lending_market_owner.rs | 10 +- .../tests/withdraw_obligation_collateral.rs | 391 +++++--- 23 files changed, 2185 insertions(+), 1844 deletions(-) delete mode 100644 token-lending/program/tests/accrue_interest.rs delete mode 100644 token-lending/program/tests/borrow.rs create mode 100644 token-lending/program/tests/borrow_obligation_liquidity.rs rename token-lending/program/tests/{deposit.rs => deposit_reserve_liquidity.rs} (89%) create mode 100644 token-lending/program/tests/fixtures/README.md create mode 100644 token-lending/program/tests/fixtures/btc_usd_aggregator.bin create mode 100644 token-lending/program/tests/fixtures/eth_usd_aggregator.bin delete mode 100644 token-lending/program/tests/liquidate.rs create mode 100644 token-lending/program/tests/liquidate_obligation.rs rename token-lending/program/tests/{withdraw.rs => redeem_reserve_collateral.rs} (64%) create mode 100644 token-lending/program/tests/refresh_obligation.rs create mode 100644 token-lending/program/tests/refresh_reserve.rs delete mode 100644 token-lending/program/tests/repay.rs create mode 100644 token-lending/program/tests/repay_obligation_liquidity.rs diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index 068f4823e17..b604e238a29 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -21,7 +21,8 @@ use solana_program::{ }; /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) -pub const INITIAL_COLLATERAL_RATIO: u64 = 5; +// @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 diff --git a/token-lending/program/tests/accrue_interest.rs b/token-lending/program/tests/accrue_interest.rs deleted file mode 100644 index 6775d70d2fa..00000000000 --- a/token-lending/program/tests/accrue_interest.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use spl_token_lending::{ - instruction::accrue_reserve_interest, - math::{Decimal, Rate, TryMul}, - processor::process_instruction, - state::SLOTS_PER_YEAR, -}; - -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - -const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; -const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "spl_token_lending", - spl_token_lending::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_bpf_compute_max_units(80_000); - - let user_accounts_owner = Keypair::new(); - let usdc_mint = add_usdc_mint(&mut test); - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - let mut reserve_config = TEST_RESERVE_CONFIG; - reserve_config.loan_to_value_ratio = 80; - - // Configure reserve to a fixed borrow rate of 1% - const BORROW_RATE: u8 = 1; - reserve_config.min_borrow_rate = BORROW_RATE; - reserve_config.optimal_borrow_rate = BORROW_RATE; - reserve_config.optimal_utilization_rate = 100; - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - borrow_amount: 100, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - slots_elapsed: SLOTS_PER_YEAR, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - borrow_amount: 100, - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - slots_elapsed: SLOTS_PER_YEAR, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[accrue_reserve_interest( - spl_token_lending::id(), - vec![usdc_reserve.pubkey, sol_reserve.pubkey], - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let sol_reserve = sol_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_reserve.get_state(&mut banks_client).await; - - let borrow_rate = Rate::from_percent(100u8 + BORROW_RATE); - assert!(sol_reserve.cumulative_borrow_rate_wads > borrow_rate.into()); - assert_eq!( - sol_reserve.cumulative_borrow_rate_wads, - usdc_reserve.cumulative_borrow_rate_wads - ); - assert!( - sol_reserve.liquidity.borrowed_amount_wads - > Decimal::from(100u64).try_mul(borrow_rate).unwrap() - ); - assert_eq!( - sol_reserve.liquidity.borrowed_amount_wads, - usdc_reserve.liquidity.borrowed_amount_wads - ); -} diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs deleted file mode 100644 index 7e04cbf1201..00000000000 --- a/token-lending/program/tests/borrow.rs +++ /dev/null @@ -1,325 +0,0 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_program_test::*; -use solana_sdk::{pubkey::Pubkey, signature::Keypair}; -use spl_token_lending::{ - instruction::AmountType, math::Decimal, processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, -}; - -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - -#[tokio::test] -async fn test_borrow_quote_currency() { - // Using SOL/USDC max 3 bids: - // $13.988, 300.0 SOL - // $13.960, 206.8 SOL - // $13.928, 1000.0 SOL - // - // Collateral amount = 750 * 0.8 (LTV) = 600 SOL - // Borrow amount = 13.988 * 300 + 13.960 * 206.8 + 13.928 * 93.2 = 8,381.4176 USDC - const SOL_COLLATERAL_AMOUNT_LAMPORTS: u64 = 750 * LAMPORTS_TO_SOL; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 8_381_417_600; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 20_000 * FRACTIONAL_TO_USDC; - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 2 * SOL_COLLATERAL_AMOUNT_LAMPORTS; - - let mut test = ProgramTest::new( - "spl_token_lending", - spl_token_lending::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_bpf_compute_max_units(118_000); - - let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); - let usdc_mint = add_usdc_mint(&mut test); - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - let mut reserve_config = TEST_RESERVE_CONFIG; - reserve_config.loan_to_value_ratio = 80; - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let usdc_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, - ); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - - let borrow_amount = - get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, 0); - - let collateral_supply = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; - assert_eq!(collateral_supply, 0); - - let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * SOL_COLLATERAL_AMOUNT_LAMPORTS; - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &sol_reserve, - borrow_reserve: &usdc_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: collateral_deposit_amount, - user_accounts_owner: &user_accounts_owner, - obligation: &usdc_obligation, - }, - ) - .await; - - let borrow_amount = - get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL); - - let borrow_fees = TEST_RESERVE_CONFIG - .fees - .calculate_borrow_fees(borrow_amount) - .unwrap() - .0; - - let collateral_supply = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; - assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); - - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &sol_reserve, - borrow_reserve: &usdc_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::ExactAmount, - amount: borrow_amount, - user_accounts_owner: &user_accounts_owner, - obligation: &usdc_obligation, - }, - ) - .await; - - let borrow_amount = - get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, 2 * USDC_BORROW_AMOUNT_FRACTIONAL); - - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; - assert_eq!(user_collateral_balance, 0); - - let collateral_deposited = 2 * collateral_deposit_amount; - let (total_fee, host_fee) = TEST_RESERVE_CONFIG - .fees - .calculate_borrow_fees(borrow_amount) - .unwrap(); - - assert!(total_fee > 0); - assert!(host_fee > 0); - - let collateral_supply = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; - assert_eq!(collateral_supply, collateral_deposited - total_fee); - - let fee_balance = - get_token_balance(&mut banks_client, sol_reserve.liquidity_fee_receiver).await; - assert_eq!(fee_balance, total_fee - host_fee); - - let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.liquidity_host).await; - assert_eq!(host_fee_balance, host_fee); -} - -#[tokio::test] -async fn test_borrow_base_currency() { - // Using SOL/USDC min 2 asks: - // $14.074, 4707.1 SOL - // $14.055, 1751.7 SOL - // $13.989, 12.1 SOL - // - // Borrow amount = 2000 SOL - // Collateral amount = 13.989 * 12.1 + 14.055 * 1751.7 + 14.074 * 236.2 = 28,113.6892 USDC - const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 2000 * LAMPORTS_TO_SOL; - const USDC_COLLATERAL_LAMPORTS: u64 = 28_113_689_200; - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 5000 * LAMPORTS_TO_SOL; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 2 * USDC_COLLATERAL_LAMPORTS; - - let mut test = ProgramTest::new( - "spl_token_lending", - spl_token_lending::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_bpf_compute_max_units(118_000); - - let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); - let usdc_mint = add_usdc_mint(&mut test); - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - let mut reserve_config = TEST_RESERVE_CONFIG; - reserve_config.loan_to_value_ratio = 100; - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let sol_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &sol_reserve, - collateral_reserve: &usdc_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, - ); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - - let borrow_amount = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, 0); - - let collateral_supply = - get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; - assert_eq!(collateral_supply, 0); - - let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * USDC_COLLATERAL_LAMPORTS; - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &usdc_reserve, - borrow_reserve: &sol_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: collateral_deposit_amount, - user_accounts_owner: &user_accounts_owner, - obligation: &sol_obligation, - }, - ) - .await; - - let borrow_amount = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS); - - let borrow_fees = TEST_RESERVE_CONFIG - .fees - .calculate_borrow_fees(borrow_amount) - .unwrap() - .0; - - let collateral_supply = - get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; - assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); - - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &usdc_reserve, - borrow_reserve: &sol_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::ExactAmount, - amount: borrow_amount, - user_accounts_owner: &user_accounts_owner, - obligation: &sol_obligation, - }, - ) - .await; - - let borrow_amount = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; - assert_eq!(borrow_amount, 2 * SOL_BORROW_AMOUNT_LAMPORTS); - - let (mut total_fee, mut host_fee) = TEST_RESERVE_CONFIG - .fees - .calculate_borrow_fees(borrow_amount) - .unwrap(); - - // avoid rounding error by assessing fees individually - total_fee *= 2; - host_fee *= 2; - - assert!(total_fee > 0); - assert!(host_fee > 0); - - let collateral_supply = - get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; - assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); - - let fee_balance = - get_token_balance(&mut banks_client, usdc_reserve.liquidity_fee_receiver).await; - assert_eq!(fee_balance, total_fee - host_fee); - - let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.liquidity_host).await; - assert_eq!(host_fee_balance, host_fee); -} diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs new file mode 100644 index 00000000000..0fac1e06aba --- /dev/null +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -0,0 +1,401 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; +use spl_token_lending::{ + error::LendingError, + instruction::{borrow_obligation_liquidity, refresh_obligation}, + math::Decimal, + processor::process_instruction, + state::{FeeCalculation, INITIAL_COLLATERAL_RATIO}, +}; + +#[tokio::test] +async fn test_borrow_quote_currency() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(41_000); + + const USDC_TOTAL_BORROW_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const FEE_AMOUNT: u64 = 100; + const HOST_FEE_AMOUNT: u64 = 20; + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_TOTAL_BORROW_FRACTIONAL - FEE_AMOUNT; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_TOTAL_BORROW_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_liquidity_supply = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + USDC_BORROW_AMOUNT_FRACTIONAL, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(usdc_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + let obligation = test_obligation.get_state(&mut banks_client).await; + + let (total_fee, host_fee) = usdc_reserve + .config + .fees + .calculate_borrow_fees( + USDC_BORROW_AMOUNT_FRACTIONAL.into(), + FeeCalculation::Exclusive, + ) + .unwrap(); + + assert_eq!(total_fee, FEE_AMOUNT); + assert_eq!(host_fee, HOST_FEE_AMOUNT); + + let borrow_amount = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL); + + let liquidity = &obligation.borrows[0]; + assert_eq!( + liquidity.borrowed_amount_wads, + Decimal::from(USDC_TOTAL_BORROW_FRACTIONAL) + ); + assert_eq!( + usdc_reserve.liquidity.borrowed_amount_wads, + liquidity.borrowed_amount_wads + ); + + let liquidity_supply = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply, + initial_liquidity_supply - USDC_TOTAL_BORROW_FRACTIONAL + ); + + let fee_balance = get_token_balance( + &mut banks_client, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + ) + .await; + assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + + let host_fee_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; + assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); +} + +#[tokio::test] +async fn test_borrow_max_base_currency() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(42_000); + + const FEE_AMOUNT: u64 = 5000; + const HOST_FEE_AMOUNT: u64 = 1000; + + const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = + 2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; + const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL; + const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; + const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS; + + let user_accounts_owner = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_liquidity_supply = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![usdc_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + u64::max_value(), + sol_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.user_liquidity_pubkey, + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(sol_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + let obligation = test_obligation.get_state(&mut banks_client).await; + + let (total_fee, host_fee) = sol_reserve + .config + .fees + .calculate_borrow_fees(SOL_BORROW_AMOUNT_LAMPORTS.into(), FeeCalculation::Inclusive) + .unwrap(); + + assert_eq!(total_fee, FEE_AMOUNT); + assert_eq!(host_fee, HOST_FEE_AMOUNT); + + let borrow_amount = + get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; + assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT); + + let liquidity = &obligation.borrows[0]; + assert_eq!( + liquidity.borrowed_amount_wads, + Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS) + ); + + let liquidity_supply = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply, + initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS + ); + + let fee_balance = get_token_balance( + &mut banks_client, + sol_test_reserve.liquidity_fee_receiver_pubkey, + ) + .await; + assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + + let host_fee_balance = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_host_pubkey).await; + assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); +} + +#[tokio::test] +async fn test_borrow_too_large() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC + 1; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + USDC_BORROW_AMOUNT_FRACTIONAL, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(usdc_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + + // check that transaction fails + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::BorrowTooLarge as u32) + ) + ); +} diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index a18ed4161e5..ed15c7473bd 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -7,17 +7,19 @@ use solana_program_test::*; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, + system_instruction::create_account, transaction::Transaction, }; -use spl_token::instruction::approve; +use spl_token::{ + instruction::approve, + solana_program::program_pack::Pack, + state::{Account as Token, Mint}, +}; use spl_token_lending::{ - instruction::deposit_obligation_collateral, math::Decimal, processor::process_instruction, + instruction::deposit_obligation_collateral, processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, }; -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - #[tokio::test] async fn test_success() { let mut test = ProgramTest::new( @@ -27,94 +29,88 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(25_000); - - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - - const DEPOSIT_COLLATERAL: u64 = 1 * LAMPORTS_TO_SOL; + test.set_bpf_compute_max_units(38_000); - const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC; - const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; let user_accounts_owner = Keypair::new(); let user_transfer_authority = Keypair::new(); + let obligation_token_mint_keypair = Keypair::new(); + let obligation_token_account_keypair = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let sol_reserve = add_reserve( + let sol_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, + user_liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, liquidity_mint_decimals: 9, liquidity_mint_pubkey: spl_token::native_mint::id(), - collateral_amount: OBLIGATION_COLLATERAL, config: TEST_RESERVE_CONFIG, + mark_fresh: true, ..AddReserveArgs::default() }, ); - let usdc_reserve = add_reserve( + let test_obligation = add_obligation( &mut test, - &user_accounts_owner, &lending_market, - AddReserveArgs { - initial_borrow_rate: 1, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: OBLIGATION_LOAN * 101 / 100, - user_liquidity_amount: OBLIGATION_LOAN, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let obligation = add_obligation( - &mut test, &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: OBLIGATION_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(OBLIGATION_LOAN), - }, + AddObligationArgs::default(), ); let (mut banks_client, payer, recent_blockhash) = test.start().await; + test_obligation.validate_state(&mut banks_client).await; + let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; - let initial_obligation_token_balance = - get_token_balance(&mut banks_client, obligation.token_account).await; + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + + let rent = banks_client.get_rent().await.unwrap(); let mut transaction = Transaction::new_with_payer( &[ + create_account( + &payer.pubkey(), + &obligation_token_mint_keypair.pubkey(), + rent.minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token::id(), + ), + create_account( + &payer.pubkey(), + &obligation_token_account_keypair.pubkey(), + rent.minimum_balance(Token::LEN), + Token::LEN as u64, + &spl_token::id(), + ), approve( &spl_token::id(), - &sol_reserve.user_collateral_account, + &sol_test_reserve.user_collateral_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - DEPOSIT_COLLATERAL, + SOL_DEPOSIT_AMOUNT_LAMPORTS, ) .unwrap(), deposit_obligation_collateral( spl_token_lending::id(), - DEPOSIT_COLLATERAL, - sol_reserve.user_collateral_account, - sol_reserve.collateral_supply, - sol_reserve.pubkey, - obligation.pubkey, - obligation.token_mint, - obligation.token_account, + SOL_DEPOSIT_AMOUNT_LAMPORTS, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.pubkey, + test_obligation.pubkey, + obligation_token_mint_keypair.pubkey(), + obligation_token_account_keypair.pubkey(), lending_market.pubkey, - lending_market.authority, + test_obligation.owner, user_transfer_authority.pubkey(), ), ], @@ -122,30 +118,33 @@ async fn test_success() { ); transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], + &vec![ + &payer, + &obligation_token_mint_keypair, + &obligation_token_account_keypair, + &user_accounts_owner, + &user_transfer_authority, + ], recent_blockhash, ); assert!(banks_client.process_transaction(transaction).await.is_ok()); // check that collateral tokens were transferred let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; assert_eq!( collateral_supply_balance, - initial_collateral_supply_balance + DEPOSIT_COLLATERAL + initial_collateral_supply_balance + SOL_DEPOSIT_AMOUNT_LAMPORTS ); let user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; assert_eq!( user_collateral_balance, - initial_user_collateral_balance - DEPOSIT_COLLATERAL + initial_user_collateral_balance - SOL_DEPOSIT_AMOUNT_LAMPORTS ); // check that obligation tokens were minted let obligation_token_balance = - get_token_balance(&mut banks_client, obligation.token_account).await; - assert_eq!( - obligation_token_balance, - initial_obligation_token_balance + DEPOSIT_COLLATERAL - ); + get_token_balance(&mut banks_client, obligation_token_account_keypair.pubkey()).await; + assert_eq!(obligation_token_balance, SOL_DEPOSIT_AMOUNT_LAMPORTS); } diff --git a/token-lending/program/tests/deposit.rs b/token-lending/program/tests/deposit_reserve_liquidity.rs similarity index 89% rename from token-lending/program/tests/deposit.rs rename to token-lending/program/tests/deposit_reserve_liquidity.rs index 52eecd6bcbd..c8b5d864e9d 100644 --- a/token-lending/program/tests/deposit.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity.rs @@ -7,8 +7,6 @@ use solana_program_test::*; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_lending::processor::process_instruction; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - #[tokio::test] async fn test_success() { let mut test = ProgramTest::new( @@ -18,22 +16,23 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(31_000); + test.set_bpf_compute_max_units(27_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let usdc_reserve = add_reserve( + let usdc_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { user_liquidity_amount: 100 * FRACTIONAL_TO_USDC, liquidity_amount: 10_000 * FRACTIONAL_TO_USDC, liquidity_mint_decimals: usdc_mint.decimals, liquidity_mint_pubkey: usdc_mint.pubkey, config: TEST_RESERVE_CONFIG, + mark_fresh: true, ..AddReserveArgs::default() }, ); @@ -45,7 +44,7 @@ async fn test_success() { &mut banks_client, &user_accounts_owner, &payer, - &usdc_reserve, + &usdc_test_reserve, 100 * FRACTIONAL_TO_USDC, ) .await; diff --git a/token-lending/program/tests/fixtures/README.md b/token-lending/program/tests/fixtures/README.md new file mode 100644 index 00000000000..a9b6a536010 --- /dev/null +++ b/token-lending/program/tests/fixtures/README.md @@ -0,0 +1,11 @@ +# fixtures + +### SOL / USDC / SRM Aggregator Accounts + + + +```shell +solana config set --url https://devnet.solana.com +solana account 9u7fuQCJCX13HhU8DZoj4bwP3vWsDFVWkurNkv7JvhYh --output-file btc_usd_aggregator.bin +solana account ELz3y7PCa6gp8GjD8TrXnggmS2bxsDrznrxAMmSz9Thq --output-file eth_usd_aggregator.bin +``` \ No newline at end of file diff --git a/token-lending/program/tests/fixtures/btc_usd_aggregator.bin b/token-lending/program/tests/fixtures/btc_usd_aggregator.bin new file mode 100644 index 0000000000000000000000000000000000000000..988f5be33b44a2ee15998097f6c7b1eb91367dc3 GIT binary patch literal 229 zcmYc)Nwz92PEo)Gm>8HD1=Jb9Ky&JopiBR4&+pir`*`~8H>#-<=I1S#v%xiFOQIR` zX{{K>;{x|rn+M#O@_3a&#RK(@WivLOxql;P%i{Hqx3L|t(|_=Y8)DFt#)(WI7L1-sWseGj<@74+4qm+}w`N;R*!r|%daPn1Oy4cX5U>F-eK7jwcdkEG;ZODV dUz7a1In^Qc|FSc`FRuPIGel!swomi!G5~#4RwMub literal 0 HcmV?d00001 diff --git a/token-lending/program/tests/fixtures/eth_usd_aggregator.bin b/token-lending/program/tests/fixtures/eth_usd_aggregator.bin new file mode 100644 index 0000000000000000000000000000000000000000..0da026f61cc04539542e18ffb2082e608e49445d GIT binary patch literal 229 zcmYc-$*?LdPEo)Gm>8HD1=Jb9Ky&JopiBR4&+pir`*`~8H>#-<=I1S#v%xiFOQIR` zX{{K>;{x|rn+M#O@_3a&#RK(@WivLOxql;P%i{Hqx3L|t(|?f83NdI~<3uJ93r4d? z+ZEjToKo%mKzq@Rx0;{)O}r*6=)8y(Xi@vYd0{;_OyBW+U~O=HF#R9%_k9<%=lk%k cN67Wa;VUa+k2V+|HtP(Izr{C8GStTb04x7e6951J literal 0 HcmV?d00001 diff --git a/token-lending/program/tests/genesis_accounts.rs b/token-lending/program/tests/genesis_accounts.rs index 85502886195..83d35ffd930 100644 --- a/token-lending/program/tests/genesis_accounts.rs +++ b/token-lending/program/tests/genesis_accounts.rs @@ -1,318 +1,296 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_sdk::signature::Keypair; -use spl_token_lending::{ - instruction::AmountType, - math::Decimal, - state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION}, -}; - -#[tokio::test] -async fn test_success() { - let (mut test, lending) = setup_test(); - - let LendingTest { - sol_usdc_dex_market, - srm_usdc_dex_market, - usdc_mint, - srm_mint, - } = lending; - - // Initialize Lending Market - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - const LAMPORTS_TO_SOL: u64 = 1_000_000_000; - const FRACTIONAL_TO_USDC: u64 = 1_000_000; - const FRACTIONAL_TO_SRM: u64 = 1_000_000; - - // Market and collateral are setup to fill two orders in the dex market at an average - // price of 2210.5 - const fn lamports_to_usdc_fractional(lamports: u64) -> u64 { - lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000 - }; - - const USER_SOL_DEPOSIT_LAMPORTS: u64 = 10_000 * LAMPORTS_TO_SOL; - const USER_SOL_COLLATERAL_LAMPORTS: u64 = 8_500 * LAMPORTS_TO_SOL; - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 32_500 * LAMPORTS_TO_SOL; - const TOTAL_SOL: u64 = USER_SOL_DEPOSIT_LAMPORTS + INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = lamports_to_usdc_fractional(TOTAL_SOL); - const INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL: u64 = 20_000 * FRACTIONAL_TO_SRM; - - let user_accounts_owner = Keypair::new(); - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - name: "usdc".to_owned(), - dex_market_pubkey: None, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - name: "sol".to_owned(), - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let srm_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - name: "srm".to_owned(), - dex_market_pubkey: Some(srm_usdc_dex_market.pubkey), - liquidity_amount: INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_decimals: srm_mint.decimals, - liquidity_mint_pubkey: srm_mint.pubkey, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let usdc_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - collateral_reserve: &sol_reserve, - borrow_reserve: &usdc_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, - ); - - let sol_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - collateral_reserve: &usdc_reserve, - borrow_reserve: &sol_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, - ); - - let srm_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - collateral_reserve: &usdc_reserve, - borrow_reserve: &srm_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, - ); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - - // Verify lending market - let lending_market_info = lending_market.get_state(&mut banks_client).await; - assert_eq!(lending_market_info.version, PROGRAM_VERSION); - assert_eq!(lending_market_info.quote_token_mint, usdc_mint.pubkey); - - // Verify reserves - usdc_reserve.validate_state(&mut banks_client).await; - sol_reserve.validate_state(&mut banks_client).await; - srm_reserve.validate_state(&mut banks_client).await; - - let usdc_liquidity_supply = - get_token_balance(&mut banks_client, usdc_reserve.liquidity_supply).await; - assert_eq!( - usdc_liquidity_supply, - INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL - ); - let user_usdc_collateral_balance = - get_token_balance(&mut banks_client, usdc_reserve.user_collateral_account).await; - assert_eq!( - user_usdc_collateral_balance, - INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL - ); - - let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply).await; - assert_eq!(sol_liquidity_supply, INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS); - let user_sol_balance = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; - assert_eq!(user_sol_balance, USER_SOL_DEPOSIT_LAMPORTS); - let user_sol_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; - assert_eq!( - user_sol_collateral_balance, - INITIAL_COLLATERAL_RATIO * INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS - ); - - // Deposit SOL - lending_market - .deposit( - &mut banks_client, - &user_accounts_owner, - &payer, - &sol_reserve, - USER_SOL_DEPOSIT_LAMPORTS, - ) - .await; - - // Verify deposit - let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply).await; - assert_eq!( - sol_liquidity_supply, - INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS + USER_SOL_DEPOSIT_LAMPORTS - ); - let user_sol_balance = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; - assert_eq!(user_sol_balance, 0); - let user_sol_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; - assert_eq!( - user_sol_collateral_balance, - INITIAL_COLLATERAL_RATIO * TOTAL_SOL - ); - - // Borrow USDC with SOL collateral - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &sol_reserve, - borrow_reserve: &usdc_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, - user_accounts_owner: &user_accounts_owner, - obligation: &usdc_obligation, - }, - ) - .await; - - // Borrow more USDC using existing obligation account - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &sol_reserve, - borrow_reserve: &usdc_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: lamports_to_usdc_fractional( - usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS - / 100, - ), - user_accounts_owner: &user_accounts_owner, - obligation: &usdc_obligation, - }, - ) - .await; - - // Deposit USDC - lending_market - .deposit( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_reserve, - 2 * INITIAL_COLLATERAL_RATIO - * lamports_to_usdc_fractional( - usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS - / 100, - ), - ) - .await; - - // Borrow SOL with USDC collateral - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &usdc_reserve, - borrow_reserve: &sol_reserve, - dex_market: &sol_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: INITIAL_COLLATERAL_RATIO - * lamports_to_usdc_fractional( - usdc_reserve.config.loan_to_value_ratio as u64 - * USER_SOL_COLLATERAL_LAMPORTS - / 100, - ), - user_accounts_owner: &user_accounts_owner, - obligation: &sol_obligation, - }, - ) - .await; - - // Borrow SRM with USDC collateral - lending_market - .borrow( - &mut banks_client, - &payer, - BorrowArgs { - deposit_reserve: &usdc_reserve, - borrow_reserve: &srm_reserve, - dex_market: &srm_usdc_dex_market, - borrow_amount_type: AmountType::PercentAmount, - amount: INITIAL_COLLATERAL_RATIO - * lamports_to_usdc_fractional( - usdc_reserve.config.loan_to_value_ratio as u64 - * USER_SOL_COLLATERAL_LAMPORTS - / 100, - ), - user_accounts_owner: &user_accounts_owner, - obligation: &srm_obligation, - }, - ) - .await; - - // Only dump the accounts if the feature is specified - #[cfg(feature = "test-dump-genesis-accounts")] - { - use helpers::genesis::GenesisAccounts; - let mut genesis_accounts = GenesisAccounts::default(); - lending_market - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - sol_reserve - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - srm_reserve - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - usdc_reserve - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - sol_usdc_dex_market - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - srm_usdc_dex_market - .add_to_genesis(&mut banks_client, &mut genesis_accounts) - .await; - genesis_accounts - .insert_upgradeable_program(spl_token_lending::id(), "spl_token_lending.so"); - genesis_accounts.write_yaml(); - } -} +// #![cfg(feature = "test-bpf")] +// +// mod helpers; +// +// use helpers::*; +// use solana_sdk::signature::Keypair; +// use spl_token_lending::{ +// instruction::AmountType, +// math::Decimal, +// state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION}, +// }; +// +// // @FIXME: test +// #[tokio::test] +// async fn test_success() { +// let (mut test, lending) = setup_test(); +// +// let LendingTest { +// sol_usdc_aggregator, +// srm_usdc_aggregator, +// usdc_mint, +// srm_mint, +// } = lending; +// +// // Initialize Lending Market +// let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); +// +// // @FIXME: dex market is gone +// // Market and collateral are setup to fill two orders in the dex market at an average +// // price of 2210.5 +// const fn lamports_to_usdc_fractional(lamports: u64) -> u64 { +// lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000 +// }; +// +// const USER_SOL_DEPOSIT_LAMPORTS: u64 = 10_000 * LAMPORTS_TO_SOL; +// const USER_SOL_COLLATERAL_LAMPORTS: u64 = 8_500 * LAMPORTS_TO_SOL; +// const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 32_500 * LAMPORTS_TO_SOL; +// const TOTAL_SOL: u64 = USER_SOL_DEPOSIT_LAMPORTS + INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS; +// const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = lamports_to_usdc_fractional(TOTAL_SOL); +// const INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL: u64 = 20_000 * FRACTIONAL_TO_SRM; +// +// let user_accounts_owner = Keypair::new(); +// +// let usdc_test_reserve = add_reserve( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddReserveArgs { +// name: "usdc".to_owned(), +// liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, +// liquidity_mint_decimals: usdc_mint.decimals, +// liquidity_mint_pubkey: usdc_mint.pubkey, +// user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, +// config: TEST_RESERVE_CONFIG, +// ..AddReserveArgs::default() +// }, +// ); +// +// let sol_test_reserve = add_reserve( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddReserveArgs { +// name: "sol".to_owned(), +// liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, +// liquidity_mint_pubkey: spl_token::native_mint::id(), +// liquidity_mint_decimals: 9, +// user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, +// config: TEST_RESERVE_CONFIG, +// ..AddReserveArgs::default() +// }, +// ); +// +// let srm_test_reserve = add_reserve( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddReserveArgs { +// name: "srm".to_owned(), +// aggregator_pair: Some(TestAggregatorPair::SRM_USDC), +// liquidity_amount: INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL, +// liquidity_mint_decimals: srm_mint.decimals, +// liquidity_mint_pubkey: srm_mint.pubkey, +// config: TEST_RESERVE_CONFIG, +// ..AddReserveArgs::default() +// }, +// ); +// +// let usdc_obligation = add_obligation( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddObligationArgs::default(), +// ); +// +// let sol_obligation = add_obligation( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddObligationArgs::default(), +// ); +// +// let srm_obligation = add_obligation( +// &mut test, +// &lending_market, +// &user_accounts_owner, +// AddObligationArgs::default(), +// ); +// +// let (mut banks_client, payer, _recent_blockhash) = test.start().await; +// +// // Verify lending market +// let lending_market_info = lending_market.get_state(&mut banks_client).await; +// assert_eq!(lending_market_info.version, PROGRAM_VERSION); +// assert_eq!(lending_market_info.quote_token_mint, usdc_mint.pubkey); +// +// // Verify reserves +// usdc_test_reserve.validate_state(&mut banks_client).await; +// sol_test_reserve.validate_state(&mut banks_client).await; +// srm_test_reserve.validate_state(&mut banks_client).await; +// +// let usdc_liquidity_supply = +// get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; +// assert_eq!( +// usdc_liquidity_supply, +// INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL +// ); +// let user_usdc_collateral_balance = +// get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; +// assert_eq!( +// user_usdc_collateral_balance, +// INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL +// ); +// +// let sol_liquidity_supply = +// get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; +// assert_eq!(sol_liquidity_supply, INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS); +// let user_sol_balance = +// get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; +// assert_eq!(user_sol_balance, USER_SOL_DEPOSIT_LAMPORTS); +// let user_sol_collateral_balance = +// get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; +// assert_eq!( +// user_sol_collateral_balance, +// INITIAL_COLLATERAL_RATIO * INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS +// ); +// +// // Deposit SOL +// lending_market +// .deposit( +// &mut banks_client, +// &user_accounts_owner, +// &payer, +// &sol_test_reserve, +// USER_SOL_DEPOSIT_LAMPORTS, +// ) +// .await; +// +// // Verify deposit +// let sol_liquidity_supply = +// get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; +// assert_eq!( +// sol_liquidity_supply, +// INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS + USER_SOL_DEPOSIT_LAMPORTS +// ); +// let user_sol_balance = +// get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; +// assert_eq!(user_sol_balance, 0); +// let user_sol_collateral_balance = +// get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; +// assert_eq!( +// user_sol_collateral_balance, +// INITIAL_COLLATERAL_RATIO * TOTAL_SOL +// ); +// +// // @FIXME: add deposit, refresh +// +// // Borrow USDC with SOL collateral +// lending_market +// .borrow( +// &mut banks_client, +// &payer, +// BorrowArgs { +// borrow_reserve: &usdc_test_reserve, +// // @FIXME: handle u64::max_value() +// liquidity_amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, +// user_accounts_owner: &user_accounts_owner, +// obligation: &usdc_obligation, +// }, +// ) +// .await; +// +// // Borrow more USDC using existing obligation account +// lending_market +// .borrow( +// &mut banks_client, +// &payer, +// BorrowArgs { +// borrow_reserve: &usdc_test_reserve, +// // @FIXME: handle u64::max_value() +// liquidity_amount: lamports_to_usdc_fractional( +// usdc_test_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS +// / 100, +// ), +// user_accounts_owner: &user_accounts_owner, +// obligation: &usdc_obligation, +// }, +// ) +// .await; +// +// // Deposit USDC +// lending_market +// .deposit( +// &mut banks_client, +// &user_accounts_owner, +// &payer, +// &usdc_test_reserve, +// // @FIXME: LTV +// 2 * INITIAL_COLLATERAL_RATIO +// * lamports_to_usdc_fractional( +// usdc_test_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS +// / 100, +// ), +// ) +// .await; +// +// // @FIXME: add deposit, refresh +// +// // Borrow SOL with USDC collateral +// lending_market +// .borrow( +// &mut banks_client, +// &payer, +// BorrowArgs { +// // @FIXME: LTV +// liquidity_amount: INITIAL_COLLATERAL_RATIO +// * lamports_to_usdc_fractional( +// usdc_test_reserve.config.loan_to_value_ratio as u64 +// * USER_SOL_COLLATERAL_LAMPORTS +// / 100, +// ), +// obligation: &sol_obligation, +// borrow_reserve: &sol_test_reserve, +// user_accounts_owner: &user_accounts_owner, +// }, +// ) +// .await; +// +// // Borrow SRM with USDC collateral +// lending_market +// .borrow( +// &mut banks_client, +// &payer, +// BorrowArgs { +// // @FIXME: LTV +// liquidity_amount: INITIAL_COLLATERAL_RATIO +// * lamports_to_usdc_fractional( +// usdc_test_reserve.config.loan_to_value_ratio as u64 +// * USER_SOL_COLLATERAL_LAMPORTS +// / 100, +// ), +// obligation: &srm_obligation, +// borrow_reserve: &srm_test_reserve, +// user_accounts_owner: &user_accounts_owner, +// }, +// ) +// .await; +// +// // Only dump the accounts if the feature is specified +// #[cfg(feature = "test-dump-genesis-accounts")] +// { +// use helpers::genesis::GenesisAccounts; +// let mut genesis_accounts = GenesisAccounts::default(); +// lending_market +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// sol_test_reserve +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// srm_test_reserve +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// usdc_test_reserve +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// sol_usdc_aggregator +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// srm_usdc_aggregator +// .add_to_genesis(&mut banks_client, &mut genesis_accounts) +// .await; +// genesis_accounts +// .insert_upgradeable_program(spl_token_lending::id(), "spl_token_lending.so"); +// genesis_accounts.write_yaml(); +// } +// } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 6b59949edbf..d83c143d716 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -1,10 +1,19 @@ #![allow(dead_code)] +pub mod genesis; + use assert_matches::*; +use flux_aggregator::{ + borsh_state::BorshState, + borsh_utils, + state::{Aggregator, AggregatorConfig, Answer}, +}; +use genesis::GenesisAccounts; use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey}; use solana_program_test::*; use solana_sdk::{ account::Account, + account_info::IntoAccountInfo, signature::{read_keypair_file, Keypair, Signer}, system_instruction::create_account, transaction::{Transaction, TransactionError}, @@ -15,28 +24,34 @@ use spl_token::{ }; use spl_token_lending::{ instruction::{ - borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, - init_obligation, init_reserve, liquidate_obligation, AmountType, + borrow_obligation_liquidity, deposit_obligation_collateral, deposit_reserve_liquidity, + init_lending_market, init_obligation, init_reserve, liquidate_obligation, + redeem_reserve_collateral, refresh_obligation, refresh_reserve, repay_obligation_liquidity, + set_lending_market_owner, withdraw_obligation_collateral, }, math::{Decimal, Rate, TryAdd, TryMul}, processor::process_instruction, state::{ - LendingMarket, NewReserveParams, Obligation, Reserve, ReserveCollateral, ReserveConfig, - ReserveFees, ReserveLiquidity, INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION, + InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, + NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, ObligationCollateral, + ObligationLiquidity, Reserve, ReserveCollateral, ReserveConfig, ReserveFees, + ReserveLiquidity, INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION, }, }; use std::str::FromStr; -pub mod genesis; -use genesis::GenesisAccounts; + +pub const LAMPORTS_TO_SOL: u64 = 1_000_000_000; +pub const FRACTIONAL_TO_USDC: u64 = 1_000_000; +pub const FRACTIONAL_TO_SRM: u64 = 1_000_000; pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig { optimal_utilization_rate: 80, - loan_to_value_ratio: 50, - liquidation_bonus: 5, - liquidation_threshold: 55, min_borrow_rate: 0, optimal_borrow_rate: 4, max_borrow_rate: 30, + loan_to_value_ratio: 50, + liquidation_threshold: 55, + liquidation_bonus: 5, fees: ReserveFees { borrow_fee_wad: 100_000_000_000, /// 0.00001% (Aave borrow fee) @@ -47,22 +62,15 @@ pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig { pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; pub const SRM_MINT: &str = "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"; -pub const SOL_USDC_MARKET: &str = "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT"; -pub const SOL_USDC_BIDS: &str = "14ivtgssEBoBjuZJtSAPKYgpUK7DmnSwuPMqJoVTSgKJ"; -pub const SOL_USDC_ASKS: &str = "CEQdAFKdycHugujQg9k2wbmxjcpdYZyVLfV9WerTnafJ"; -pub const SRM_USDC_MARKET: &str = "ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA"; -pub const SRM_USDC_BIDS: &str = "AuL9JzRJ55MdqzubK4EutJgAumtkuFcRVuPUvTX39pN8"; -pub const SRM_USDC_ASKS: &str = "8Lx9U9wdE3afdqih1mCAXy3unJDfzSaXFqAvoLMjhwoD"; - #[allow(non_camel_case_types)] -pub enum TestDexMarketPair { +pub enum TestAggregatorPair { SRM_USDC, SOL_USDC, } pub struct LendingTest { - pub sol_usdc_dex_market: TestDexMarket, - pub srm_usdc_dex_market: TestDexMarket, + pub sol_usdc_aggregator: TestAggregator, + pub srm_usdc_aggregator: TestAggregator, pub usdc_mint: TestQuoteMint, pub srm_mint: TestQuoteMint, } @@ -77,14 +85,14 @@ pub fn setup_test() -> (ProgramTest, LendingTest) { let usdc_mint = add_usdc_mint(&mut test); let srm_mint = add_srm_mint(&mut test); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); - let srm_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SRM_USDC); + let sol_usdc_aggregator = add_aggregator(&mut test, TestAggregatorPair::SOL_USDC); + let srm_usdc_aggregator = add_aggregator(&mut test, TestAggregatorPair::SRM_USDC); ( test, LendingTest { - sol_usdc_dex_market, - srm_usdc_dex_market, + sol_usdc_aggregator, + srm_usdc_aggregator, usdc_mint, srm_mint, }, @@ -116,111 +124,158 @@ impl AddPacked for ProgramTest { } pub fn add_lending_market(test: &mut ProgramTest, quote_token_mint: Pubkey) -> TestLendingMarket { - let pubkey = Pubkey::new_unique(); - let (authority, bump_seed) = - Pubkey::find_program_address(&[pubkey.as_ref()], &spl_token_lending::id()); + let lending_market_pubkey = Pubkey::new_unique(); + let (lending_market_authority, bump_seed) = + Pubkey::find_program_address(&[lending_market_pubkey.as_ref()], &spl_token_lending::id()); - let owner = read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); + let lending_market_owner = + read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); test.add_packable_account( - pubkey, + lending_market_pubkey, u32::MAX as u64, - &LendingMarket { - version: PROGRAM_VERSION, + &LendingMarket::new(InitLendingMarketParams { bump_seed, - owner: owner.pubkey(), + owner: lending_market_owner.pubkey(), quote_token_mint, token_program_id: spl_token::id(), - }, + }), &spl_token_lending::id(), ); TestLendingMarket { - pubkey, - owner, - authority, + pubkey: lending_market_pubkey, + owner: lending_market_owner, + authority: lending_market_authority, quote_token_mint, } } +#[derive(Default)] pub struct AddObligationArgs<'a> { - pub borrow_reserve: &'a TestReserve, - pub collateral_reserve: &'a TestReserve, - pub collateral_amount: u64, - pub borrowed_liquidity_wads: Decimal, + pub deposits: &'a [(&'a TestReserve, u64)], + pub borrows: &'a [(&'a TestReserve, u64)], + pub mark_fresh: bool, + pub slots_elapsed: u64, } pub fn add_obligation( test: &mut ProgramTest, - user_accounts_owner: &Keypair, lending_market: &TestLendingMarket, + user_accounts_owner: &Keypair, args: AddObligationArgs, ) -> TestObligation { let AddObligationArgs { - borrow_reserve, - collateral_reserve, - collateral_amount, - borrowed_liquidity_wads, + deposits, + borrows, + mark_fresh, + slots_elapsed, } = args; - let token_mint_pubkey = Pubkey::new_unique(); - test.add_packable_account( - token_mint_pubkey, - u32::MAX as u64, - &Mint { - is_initialized: true, - decimals: collateral_reserve.liquidity_mint_decimals, - mint_authority: COption::Some(lending_market.authority), - supply: collateral_amount, - ..Mint::default() - }, - &spl_token::id(), - ); - - let token_account_pubkey = Pubkey::new_unique(); - test.add_packable_account( - token_account_pubkey, - u32::MAX as u64, - &Token { - mint: token_mint_pubkey, - owner: user_accounts_owner.pubkey(), - state: AccountState::Initialized, - amount: collateral_amount, - ..Token::default() - }, - &spl_token::id(), - ); - let obligation_keypair = Keypair::new(); let obligation_pubkey = obligation_keypair.pubkey(); + + let (obligation_deposits, test_deposits) = deposits + .iter() + .map(|(deposit_reserve, collateral_amount)| { + let token_mint_pubkey = Pubkey::new_unique(); + test.add_packable_account( + token_mint_pubkey, + u32::MAX as u64, + &Mint { + is_initialized: true, + decimals: deposit_reserve.liquidity_mint_decimals, + mint_authority: COption::Some(lending_market.authority), + supply: *collateral_amount, + ..Mint::default() + }, + &spl_token::id(), + ); + + let token_account_pubkey = Pubkey::new_unique(); + test.add_packable_account( + token_account_pubkey, + u32::MAX as u64, + &Token { + mint: token_mint_pubkey, + owner: user_accounts_owner.pubkey(), + state: AccountState::Initialized, + amount: *collateral_amount, + ..Token::default() + }, + &spl_token::id(), + ); + + let mut collateral = + ObligationCollateral::new(deposit_reserve.pubkey, token_mint_pubkey); + collateral.deposited_amount = *collateral_amount; + + ( + collateral, + TestObligationCollateral { + obligation_pubkey, + deposit_reserve: deposit_reserve.pubkey, + token_mint: token_mint_pubkey, + token_account: token_account_pubkey, + deposited_amount: *collateral_amount, + }, + ) + }) + .unzip(); + + let (obligation_borrows, test_borrows) = borrows + .iter() + .map(|(borrow_reserve, liquidity_amount)| { + let borrowed_amount_wads = Decimal::from(*liquidity_amount); + + let mut liquidity = ObligationLiquidity::new(borrow_reserve.pubkey); + liquidity.borrowed_amount_wads = borrowed_amount_wads; + + ( + liquidity, + TestObligationLiquidity { + obligation_pubkey, + borrow_reserve: borrow_reserve.pubkey, + borrowed_amount_wads, + }, + ) + }) + .unzip(); + + let current_slot = slots_elapsed + 1; + + let mut obligation = Obligation::new(InitObligationParams { + // intentionally wrapped to simulate elapsed slots + current_slot, + lending_market: lending_market.pubkey, + owner: user_accounts_owner.pubkey(), + deposits: obligation_deposits, + borrows: obligation_borrows, + }); + + if mark_fresh { + obligation.last_update.update_slot(current_slot); + } + test.add_packable_account( obligation_pubkey, u32::MAX as u64, - &Obligation { - version: PROGRAM_VERSION, - deposited_collateral_tokens: collateral_amount, - collateral_reserve: collateral_reserve.pubkey, - cumulative_borrow_rate_wads: Decimal::one(), - borrowed_liquidity_wads, - borrow_reserve: borrow_reserve.pubkey, - token_mint: token_mint_pubkey, - }, + &obligation, &spl_token_lending::id(), ); TestObligation { pubkey: obligation_pubkey, - token_mint: token_mint_pubkey, - token_account: token_account_pubkey, - borrow_reserve: borrow_reserve.pubkey, - collateral_reserve: collateral_reserve.pubkey, + lending_market: lending_market.pubkey, + owner: user_accounts_owner.pubkey(), + deposits: test_deposits, + borrows: test_borrows, } } #[derive(Default)] pub struct AddReserveArgs { pub name: String, - pub slots_elapsed: u64, pub config: ReserveConfig, pub liquidity_amount: u64, pub liquidity_mint_pubkey: Pubkey, @@ -230,18 +285,19 @@ pub struct AddReserveArgs { pub initial_borrow_rate: u8, pub collateral_amount: u64, pub fees_amount: u64, - pub dex_market_pubkey: Option, + pub mark_fresh: bool, + pub slots_elapsed: u64, + pub aggregator_pair: Option, } pub fn add_reserve( test: &mut ProgramTest, - user_accounts_owner: &Keypair, lending_market: &TestLendingMarket, + user_accounts_owner: &Keypair, args: AddReserveArgs, ) -> TestReserve { let AddReserveArgs { name, - slots_elapsed, config, liquidity_amount, liquidity_mint_pubkey, @@ -251,15 +307,32 @@ pub fn add_reserve( initial_borrow_rate, collateral_amount, fees_amount, - dex_market_pubkey, + mark_fresh, + slots_elapsed, + aggregator_pair, } = args; + let (liquidity_aggregator_pubkey, market_price) = if let Some(aggregator_pair) = aggregator_pair + { + let aggregator = add_aggregator(test, aggregator_pair); + (Some(aggregator.pubkey), aggregator.price) + } else if liquidity_mint_pubkey == spl_token::native_mint::id() { + let aggregator = add_aggregator(test, TestAggregatorPair::SOL_USDC); + (Some(aggregator.pubkey), aggregator.price) + } else if liquidity_mint_pubkey == lending_market.quote_token_mint { + (None, 1 * FRACTIONAL_TO_USDC) + } else { + panic!("aggregator pair is required"); + }; + let is_native = if liquidity_mint_pubkey == spl_token::native_mint::id() { COption::Some(1) } else { COption::None }; + let current_slot = slots_elapsed + 1; + let collateral_mint_pubkey = Pubkey::new_unique(); test.add_packable_account( collateral_mint_pubkey, @@ -339,29 +412,35 @@ pub fn add_reserve( let reserve_keypair = Keypair::new(); let reserve_pubkey = reserve_keypair.pubkey(); - let reserve_liquidity = ReserveLiquidity::new( - liquidity_mint_pubkey, - liquidity_mint_decimals, - liquidity_supply_pubkey, - liquidity_fee_receiver_pubkey, - ); - let reserve_collateral = - ReserveCollateral::new(collateral_mint_pubkey, collateral_supply_pubkey); - let mut reserve = Reserve::new(NewReserveParams { - // intentionally wrapped to simulate elapsed slots - current_slot: 1u64.wrapping_sub(slots_elapsed), + let mut reserve = Reserve::new(InitReserveParams { + current_slot, lending_market: lending_market.pubkey, - dex_market: dex_market_pubkey.into(), - liquidity: reserve_liquidity, - collateral: reserve_collateral, + liquidity: ReserveLiquidity::new(NewReserveLiquidityParams { + mint_pubkey: liquidity_mint_pubkey, + mint_decimals: liquidity_mint_decimals, + supply_pubkey: liquidity_supply_pubkey, + fee_receiver: liquidity_fee_receiver_pubkey, + aggregator: liquidity_aggregator_pubkey.into(), + market_price, + }), + collateral: ReserveCollateral::new(NewReserveCollateralParams { + mint_pubkey: collateral_mint_pubkey, + supply_pubkey: collateral_supply_pubkey, + }), config, }); reserve.deposit_liquidity(liquidity_amount).unwrap(); - reserve.liquidity.borrow(borrow_amount).unwrap(); + reserve.liquidity.borrow(borrow_amount.into()).unwrap(); let borrow_rate_multiplier = Rate::one() .try_add(Rate::from_percent(initial_borrow_rate)) .unwrap(); - reserve.cumulative_borrow_rate_wads = Decimal::one().try_mul(borrow_rate_multiplier).unwrap(); + reserve.liquidity.cumulative_borrow_rate_wads = + Decimal::one().try_mul(borrow_rate_multiplier).unwrap(); + + if mark_fresh { + reserve.last_update.update_slot(current_slot); + } + test.add_packable_account( reserve_pubkey, u32::MAX as u64, @@ -406,18 +485,19 @@ pub fn add_reserve( TestReserve { name, pubkey: reserve_pubkey, - lending_market: lending_market.pubkey, + lending_market_pubkey: lending_market.pubkey, config, - liquidity_mint: liquidity_mint_pubkey, + liquidity_mint_pubkey, liquidity_mint_decimals, - liquidity_supply: liquidity_supply_pubkey, - liquidity_fee_receiver: liquidity_fee_receiver_pubkey, - liquidity_host: liquidity_host_pubkey, - collateral_mint: collateral_mint_pubkey, - collateral_supply: collateral_supply_pubkey, - user_liquidity_account: user_liquidity_pubkey, - user_collateral_account: user_collateral_pubkey, - dex_market: dex_market_pubkey, + liquidity_supply_pubkey, + liquidity_fee_receiver_pubkey, + liquidity_host_pubkey, + collateral_mint_pubkey, + collateral_supply_pubkey, + user_liquidity_pubkey, + user_collateral_pubkey, + liquidity_aggregator_pubkey, + market_price, } } @@ -429,21 +509,17 @@ pub struct TestLendingMarket { } pub struct BorrowArgs<'a> { - pub deposit_reserve: &'a TestReserve, + pub liquidity_amount: u64, + pub obligation: &'a TestObligation, pub borrow_reserve: &'a TestReserve, - pub borrow_amount_type: AmountType, - pub amount: u64, - pub dex_market: &'a TestDexMarket, pub user_accounts_owner: &'a Keypair, - pub obligation: &'a TestObligation, } pub struct LiquidateArgs<'a> { + pub liquidity_amount: u64, + pub obligation: &'a TestObligation, pub repay_reserve: &'a TestReserve, pub withdraw_reserve: &'a TestReserve, - pub obligation: &'a TestObligation, - pub amount: u64, - pub dex_market: &'a TestDexMarket, pub user_accounts_owner: &'a Keypair, } @@ -453,26 +529,29 @@ impl TestLendingMarket { quote_token_mint: Pubkey, payer: &Keypair, ) -> Self { - let owner = read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); - let keypair = Keypair::new(); - let pubkey = keypair.pubkey(); - let (authority_pubkey, _bump_seed) = - Pubkey::find_program_address(&[&pubkey.to_bytes()[..32]], &spl_token_lending::id()); + let lending_market_owner = + read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); + let lending_market_keypair = Keypair::new(); + let lending_market_pubkey = lending_market_keypair.pubkey(); + let (lending_market_authority, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..32]], + &spl_token_lending::id(), + ); let rent = banks_client.get_rent().await.unwrap(); let mut transaction = Transaction::new_with_payer( &[ create_account( &payer.pubkey(), - &pubkey, + &lending_market_pubkey, rent.minimum_balance(LendingMarket::LEN), LendingMarket::LEN as u64, &spl_token_lending::id(), ), init_lending_market( spl_token_lending::id(), - pubkey, - owner.pubkey(), + lending_market_pubkey, + lending_market_owner.pubkey(), quote_token_mint, ), ], @@ -480,47 +559,67 @@ impl TestLendingMarket { ); let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); - transaction.sign(&[&payer, &keypair], recent_blockhash); + transaction.sign(&[&payer, &lending_market_keypair], recent_blockhash); assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); TestLendingMarket { - owner, - pubkey, - authority: authority_pubkey, + owner: lending_market_owner, + pubkey: lending_market_pubkey, + authority: lending_market_authority, quote_token_mint, } } + pub async fn refresh_reserve( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + reserve: &TestReserve, + ) { + let mut transaction = Transaction::new_with_payer( + &[refresh_reserve( + spl_token_lending::id(), + reserve.pubkey, + reserve.liquidity_aggregator_pubkey, + )], + Some(&payer.pubkey()), + ); + + let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); + transaction.sign(&[payer], recent_blockhash); + + assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); + } + pub async fn deposit( &self, banks_client: &mut BanksClient, user_accounts_owner: &Keypair, payer: &Keypair, reserve: &TestReserve, - amount: u64, + liquidity_amount: u64, ) { let user_transfer_authority = Keypair::new(); let mut transaction = Transaction::new_with_payer( &[ approve( &spl_token::id(), - &reserve.user_liquidity_account, + &reserve.user_liquidity_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - amount, + liquidity_amount, ) .unwrap(), deposit_reserve_liquidity( spl_token_lending::id(), - amount, - reserve.user_liquidity_account, - reserve.user_collateral_account, + liquidity_amount, + reserve.user_liquidity_pubkey, + reserve.user_collateral_pubkey, reserve.pubkey, - reserve.liquidity_supply, - reserve.collateral_mint, + reserve.liquidity_supply_pubkey, + reserve.collateral_mint_pubkey, self.pubkey, - self.authority, user_transfer_authority.pubkey(), ), ], @@ -543,56 +642,37 @@ impl TestLendingMarket { args: LiquidateArgs<'_>, ) { let LiquidateArgs { + liquidity_amount, + obligation, repay_reserve, withdraw_reserve, - obligation, - amount, - dex_market, user_accounts_owner, } = args; - let dex_market_orders_pubkey = if repay_reserve.dex_market.is_none() { - dex_market.asks_pubkey - } else { - dex_market.bids_pubkey - }; - - let memory_keypair = Keypair::new(); let user_transfer_authority = Keypair::new(); let mut transaction = Transaction::new_with_payer( &[ - create_account( - &payer.pubkey(), - &memory_keypair.pubkey(), - 0, - 65548, - &spl_token_lending::id(), - ), approve( &spl_token::id(), - &repay_reserve.user_liquidity_account, + &repay_reserve.user_liquidity_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - amount, + liquidity_amount, ) .unwrap(), liquidate_obligation( spl_token_lending::id(), - amount, - repay_reserve.user_liquidity_account, - withdraw_reserve.user_collateral_account, + liquidity_amount, + repay_reserve.user_liquidity_pubkey, + withdraw_reserve.user_collateral_pubkey, repay_reserve.pubkey, - repay_reserve.liquidity_supply, + repay_reserve.liquidity_supply_pubkey, withdraw_reserve.pubkey, - withdraw_reserve.collateral_supply, + withdraw_reserve.collateral_supply_pubkey, obligation.pubkey, self.pubkey, - self.authority, user_transfer_authority.pubkey(), - dex_market.pubkey, - dex_market_orders_pubkey, - memory_keypair.pubkey(), ), ], Some(&payer.pubkey()), @@ -600,12 +680,7 @@ impl TestLendingMarket { let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); transaction.sign( - &[ - &payer, - &memory_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], + &[&payer, &user_accounts_owner, &user_transfer_authority], recent_blockhash, ); assert!(banks_client.process_transaction(transaction).await.is_ok()); @@ -617,85 +692,31 @@ impl TestLendingMarket { payer: &Keypair, args: BorrowArgs<'_>, ) { - let memory_keypair = Keypair::new(); - let user_transfer_authority = Keypair::new(); - let BorrowArgs { + liquidity_amount, + obligation, borrow_reserve, - deposit_reserve, - borrow_amount_type, - amount, - dex_market, user_accounts_owner, - obligation, } = args; - let dex_market_orders_pubkey = if deposit_reserve.dex_market.is_none() { - dex_market.asks_pubkey - } else { - dex_market.bids_pubkey - }; - - let approve_amount = if borrow_amount_type == AmountType::PercentAmount { - amount - } else { - get_token_balance(banks_client, deposit_reserve.user_collateral_account).await - }; - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &deposit_reserve.user_collateral_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - approve_amount, - ) - .unwrap(), - create_account( - &payer.pubkey(), - &memory_keypair.pubkey(), - 0, - 65548, - &spl_token_lending::id(), - ), - borrow_obligation_liquidity( - spl_token_lending::id(), - amount, - borrow_amount_type, - deposit_reserve.user_collateral_account, - borrow_reserve.user_liquidity_account, - deposit_reserve.pubkey, - deposit_reserve.collateral_supply, - deposit_reserve.liquidity_fee_receiver, - borrow_reserve.pubkey, - borrow_reserve.liquidity_supply, - self.pubkey, - self.authority, - user_transfer_authority.pubkey(), - obligation.pubkey, - obligation.token_mint, - obligation.token_account, - dex_market.pubkey, - dex_market_orders_pubkey, - memory_keypair.pubkey(), - Some(deposit_reserve.liquidity_host), - ), - ], + &[borrow_obligation_liquidity( + spl_token_lending::id(), + liquidity_amount, + borrow_reserve.liquidity_supply_pubkey, + borrow_reserve.user_liquidity_pubkey, + borrow_reserve.pubkey, + borrow_reserve.liquidity_fee_receiver_pubkey, + obligation.pubkey, + self.pubkey, + obligation.owner, + Some(borrow_reserve.liquidity_host_pubkey), + )], Some(&payer.pubkey()), ); let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); - transaction.sign( - &vec![ - payer, - user_accounts_owner, - &memory_keypair, - &user_transfer_authority, - ], - recent_blockhash, - ); + transaction.sign(&vec![payer, user_accounts_owner], recent_blockhash); assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); } @@ -709,6 +730,13 @@ impl TestLendingMarket { LendingMarket::unpack(&lending_market_account.data[..]).unwrap() } + pub async fn validate_state(&self, banks_client: &mut BanksClient) { + let lending_market = self.get_state(banks_client).await; + assert_eq!(lending_market.version, PROGRAM_VERSION); + assert_eq!(lending_market.owner, self.owner.pubkey()); + assert_eq!(lending_market.quote_token_mint, self.quote_token_mint); + } + pub async fn add_to_genesis( &self, banks_client: &mut BanksClient, @@ -725,18 +753,19 @@ impl TestLendingMarket { pub struct TestReserve { pub name: String, pub pubkey: Pubkey, - pub lending_market: Pubkey, + pub lending_market_pubkey: Pubkey, pub config: ReserveConfig, - pub liquidity_mint: Pubkey, + pub liquidity_mint_pubkey: Pubkey, pub liquidity_mint_decimals: u8, - pub liquidity_supply: Pubkey, - pub collateral_mint: Pubkey, - pub collateral_supply: Pubkey, - pub liquidity_fee_receiver: Pubkey, - pub liquidity_host: Pubkey, - pub user_liquidity_account: Pubkey, - pub user_collateral_account: Pubkey, - pub dex_market: Option, + pub liquidity_supply_pubkey: Pubkey, + pub liquidity_fee_receiver_pubkey: Pubkey, + pub liquidity_host_pubkey: Pubkey, + pub collateral_mint_pubkey: Pubkey, + pub collateral_supply_pubkey: Pubkey, + pub user_liquidity_pubkey: Pubkey, + pub user_collateral_pubkey: Pubkey, + pub liquidity_aggregator_pubkey: Option, + pub market_price: u64, } impl TestReserve { @@ -745,13 +774,13 @@ impl TestReserve { name: String, banks_client: &mut BanksClient, lending_market: &TestLendingMarket, - reserve_amount: u64, + liquidity_amount: u64, config: ReserveConfig, liquidity_mint_pubkey: Pubkey, - user_liquidity_account: Pubkey, + user_liquidity_pubkey: Pubkey, payer: &Keypair, user_accounts_owner: &Keypair, - dex_market: &TestDexMarket, + aggregator: Option<&TestAggregator>, ) -> Result { let reserve_keypair = Keypair::new(); let reserve_pubkey = reserve_keypair.pubkey(); @@ -763,10 +792,12 @@ impl TestReserve { let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority_keypair = Keypair::new(); - let dex_market_pubkey = if liquidity_mint_pubkey != lending_market.quote_token_mint { - Some(dex_market.pubkey) + let (liquidity_aggregator_pubkey, market_price) = if let Some(aggregator) = aggregator { + (Some(aggregator.pubkey), aggregator.price) + } else if liquidity_mint_pubkey == lending_market.quote_token_mint { + (None, 1 * FRACTIONAL_TO_USDC) } else { - None + panic!("aggregator is required"); }; let liquidity_mint_account = banks_client @@ -781,11 +812,11 @@ impl TestReserve { &[ approve( &spl_token::id(), - &user_liquidity_account, + &user_liquidity_pubkey, &user_transfer_authority_keypair.pubkey(), &user_accounts_owner.pubkey(), &[], - reserve_amount, + liquidity_amount, ) .unwrap(), create_account( @@ -839,20 +870,20 @@ impl TestReserve { ), init_reserve( spl_token_lending::id(), - reserve_amount, + liquidity_amount, config, - user_liquidity_account, + user_liquidity_pubkey, user_collateral_token_keypair.pubkey(), reserve_pubkey, liquidity_mint_pubkey, liquidity_supply_keypair.pubkey(), - liquidity_mint_pubkey, - collateral_supply_keypair.pubkey(), liquidity_fee_receiver_keypair.pubkey(), + collateral_mint_keypair.pubkey(), + collateral_supply_keypair.pubkey(), lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority_keypair.pubkey(), - dex_market_pubkey, + liquidity_aggregator_pubkey, ), ], Some(&payer.pubkey()), @@ -882,18 +913,19 @@ impl TestReserve { .map(|_| Self { name, pubkey: reserve_pubkey, - lending_market: lending_market.pubkey, + lending_market_pubkey: lending_market.pubkey, config, - liquidity_mint: liquidity_mint_pubkey, + liquidity_mint_pubkey, liquidity_mint_decimals: liquidity_mint.decimals, - liquidity_supply: liquidity_supply_keypair.pubkey(), - liquidity_fee_receiver: liquidity_fee_receiver_keypair.pubkey(), - liquidity_host: liquidity_host_keypair.pubkey(), - collateral_mint: collateral_mint_keypair.pubkey(), - collateral_supply: collateral_supply_keypair.pubkey(), - user_liquidity_account, - user_collateral_account: user_collateral_token_keypair.pubkey(), - dex_market: dex_market_pubkey, + liquidity_supply_pubkey: liquidity_supply_keypair.pubkey(), + liquidity_fee_receiver_pubkey: liquidity_fee_receiver_keypair.pubkey(), + liquidity_host_pubkey: liquidity_host_keypair.pubkey(), + collateral_mint_pubkey: collateral_mint_keypair.pubkey(), + collateral_supply_pubkey: collateral_supply_keypair.pubkey(), + user_liquidity_pubkey, + user_collateral_pubkey: user_collateral_token_keypair.pubkey(), + liquidity_aggregator_pubkey, + market_price, }) .map_err(|e| e.unwrap()) } @@ -907,47 +939,56 @@ impl TestReserve { genesis_accounts .fetch_and_insert(banks_client, self.pubkey) .await; - println!("{}_collateral_mint: {}", self.name, self.collateral_mint); + println!( + "{}_collateral_mint: {}", + self.name, self.collateral_mint_pubkey + ); genesis_accounts - .fetch_and_insert(banks_client, self.collateral_mint) + .fetch_and_insert(banks_client, self.collateral_mint_pubkey) .await; println!( "{}_collateral_supply: {}", - self.name, self.collateral_supply + self.name, self.collateral_supply_pubkey ); genesis_accounts - .fetch_and_insert(banks_client, self.liquidity_fee_receiver) + .fetch_and_insert(banks_client, self.liquidity_fee_receiver_pubkey) .await; println!( "{}_liquidity_fee_receiver: {}", - self.name, self.liquidity_fee_receiver + self.name, self.liquidity_fee_receiver_pubkey ); genesis_accounts - .fetch_and_insert(banks_client, self.collateral_supply) + .fetch_and_insert(banks_client, self.collateral_supply_pubkey) .await; if &self.name != "sol" { - println!("{}_liquidity_mint: {}", self.name, self.liquidity_mint); + println!( + "{}_liquidity_mint: {}", + self.name, self.liquidity_mint_pubkey + ); genesis_accounts - .fetch_and_insert(banks_client, self.liquidity_mint) + .fetch_and_insert(banks_client, self.liquidity_mint_pubkey) .await; } - println!("{}_liquidity_supply: {}", self.name, self.liquidity_supply); + println!( + "{}_liquidity_supply: {}", + self.name, self.liquidity_supply_pubkey + ); genesis_accounts - .fetch_and_insert(banks_client, self.liquidity_supply) + .fetch_and_insert(banks_client, self.liquidity_supply_pubkey) .await; println!( "{}_user_collateral: {}", - self.name, self.user_collateral_account + self.name, self.user_collateral_pubkey ); genesis_accounts - .fetch_and_insert(banks_client, self.user_collateral_account) + .fetch_and_insert(banks_client, self.user_collateral_pubkey) .await; println!( "{}_user_liquidity: {}", - self.name, self.user_liquidity_account + self.name, self.user_liquidity_pubkey ); genesis_accounts - .fetch_and_insert(banks_client, self.user_liquidity_account) + .fetch_and_insert(banks_client, self.user_liquidity_pubkey) .await; } @@ -962,23 +1003,33 @@ impl TestReserve { pub async fn validate_state(&self, banks_client: &mut BanksClient) { let reserve = self.get_state(banks_client).await; - assert!(reserve.last_update_slot > 0); + assert!(reserve.last_update.slot > 0); assert_eq!(PROGRAM_VERSION, reserve.version); - assert_eq!(self.lending_market, reserve.lending_market); - assert_eq!(self.liquidity_mint, reserve.liquidity.mint_pubkey); - assert_eq!(self.liquidity_supply, reserve.liquidity.supply_pubkey); - assert_eq!(self.collateral_mint, reserve.collateral.mint_pubkey); - assert_eq!(self.collateral_supply, reserve.collateral.supply_pubkey); + assert_eq!(self.lending_market_pubkey, reserve.lending_market); + assert_eq!(self.liquidity_mint_pubkey, reserve.liquidity.mint_pubkey); + assert_eq!( + self.liquidity_supply_pubkey, + reserve.liquidity.supply_pubkey + ); + assert_eq!(self.collateral_mint_pubkey, reserve.collateral.mint_pubkey); + assert_eq!( + self.collateral_supply_pubkey, + reserve.collateral.supply_pubkey + ); assert_eq!(self.config, reserve.config); - let dex_market_coption = if let Some(dex_market_pubkey) = self.dex_market { - COption::Some(dex_market_pubkey) - } else { - COption::None - }; - - assert_eq!(dex_market_coption, reserve.dex_market); - assert_eq!(reserve.cumulative_borrow_rate_wads, Decimal::one()); + let liquidity_aggregator_coption = + if let Some(liquidity_aggregator_pubkey) = self.liquidity_aggregator_pubkey { + COption::Some(liquidity_aggregator_pubkey) + } else { + COption::None + }; + + assert_eq!(liquidity_aggregator_coption, reserve.liquidity.aggregator); + assert_eq!( + reserve.liquidity.cumulative_borrow_rate_wads, + Decimal::one() + ); assert_eq!(reserve.liquidity.borrowed_amount_wads, Decimal::zero()); assert!(reserve.liquidity.available_amount > 0); assert!(reserve.collateral.mint_total_supply > 0); @@ -988,10 +1039,10 @@ impl TestReserve { #[derive(Debug)] pub struct TestObligation { pub pubkey: Pubkey, - pub token_mint: Pubkey, - pub token_account: Pubkey, - pub collateral_reserve: Pubkey, - pub borrow_reserve: Pubkey, + pub lending_market: Pubkey, + pub owner: Pubkey, + pub deposits: Vec, + pub borrows: Vec, } impl TestObligation { @@ -999,39 +1050,21 @@ impl TestObligation { pub async fn init( banks_client: &mut BanksClient, lending_market: &TestLendingMarket, - deposit_reserve: &TestReserve, - borrow_reserve: &TestReserve, - payer: &Keypair, user_accounts_owner: &Keypair, + payer: &Keypair, ) -> Result { let obligation_keypair = Keypair::new(); - let obligation_token_mint_keypair = Keypair::new(); - let obligation_token_account_keypair = Keypair::new(); let obligation = TestObligation { pubkey: obligation_keypair.pubkey(), - token_mint: obligation_token_mint_keypair.pubkey(), - token_account: obligation_token_account_keypair.pubkey(), - collateral_reserve: deposit_reserve.pubkey, - borrow_reserve: borrow_reserve.pubkey, + lending_market: lending_market.pubkey, + owner: user_accounts_owner.pubkey(), + deposits: vec![], + borrows: vec![], }; let rent = banks_client.get_rent().await.unwrap(); let mut transaction = Transaction::new_with_payer( &[ - create_account( - &payer.pubkey(), - &obligation_token_mint_keypair.pubkey(), - rent.minimum_balance(Mint::LEN), - Mint::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &obligation_token_account_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), create_account( &payer.pubkey(), &obligation_keypair.pubkey(), @@ -1042,10 +1075,8 @@ impl TestObligation { init_obligation( spl_token_lending::id(), obligation.pubkey, - obligation.token_mint, - obligation.token_account, - user_accounts_owner.pubkey(), lending_market.pubkey, + user_accounts_owner.pubkey(), ), ], Some(&payer.pubkey()), @@ -1053,12 +1084,7 @@ impl TestObligation { let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); transaction.sign( - &vec![ - payer, - &obligation_keypair, - &obligation_token_account_keypair, - &obligation_token_mint_keypair, - ], + &vec![payer, &obligation_keypair, user_accounts_owner], recent_blockhash, ); @@ -1082,10 +1108,67 @@ impl TestObligation { pub async fn validate_state(&self, banks_client: &mut BanksClient) { let obligation = self.get_state(banks_client).await; assert_eq!(obligation.version, PROGRAM_VERSION); - assert_eq!(obligation.collateral_reserve, self.collateral_reserve); - assert!(obligation.cumulative_borrow_rate_wads >= Decimal::one()); - assert_eq!(obligation.borrow_reserve, self.borrow_reserve); - assert_eq!(obligation.token_mint, self.token_mint); + assert_eq!(obligation.lending_market, self.lending_market); + assert_eq!(obligation.owner, self.owner); + } +} + +#[derive(Debug)] +pub struct TestObligationCollateral { + pub obligation_pubkey: Pubkey, + pub deposit_reserve: Pubkey, + pub token_mint: Pubkey, + pub token_account: Pubkey, + pub deposited_amount: u64, +} + +impl TestObligationCollateral { + pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation { + let obligation_account: Account = banks_client + .get_account(self.obligation_pubkey) + .await + .unwrap() + .unwrap(); + Obligation::unpack(&obligation_account.data[..]).unwrap() + } + + pub async fn validate_state(&self, banks_client: &mut BanksClient) { + let obligation = self.get_state(banks_client).await; + assert_eq!(obligation.version, PROGRAM_VERSION); + + let (collateral, _) = obligation + .find_collateral_in_deposits(self.deposit_reserve) + .unwrap(); + assert_eq!(collateral.token_mint, self.token_mint); + assert_eq!(collateral.deposited_amount, self.deposited_amount); + } +} + +#[derive(Debug)] +pub struct TestObligationLiquidity { + pub obligation_pubkey: Pubkey, + pub borrow_reserve: Pubkey, + pub borrowed_amount_wads: Decimal, +} + +impl TestObligationLiquidity { + pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation { + let obligation_account: Account = banks_client + .get_account(self.obligation_pubkey) + .await + .unwrap() + .unwrap(); + Obligation::unpack(&obligation_account.data[..]).unwrap() + } + + pub async fn validate_state(&self, banks_client: &mut BanksClient) { + let obligation = self.get_state(banks_client).await; + assert_eq!(obligation.version, PROGRAM_VERSION); + let (liquidity, _) = obligation + .find_liquidity_in_borrows(self.borrow_reserve) + .unwrap(); + assert!(liquidity.cumulative_borrow_rate_wads >= Decimal::one()); + assert!(liquidity.borrowed_amount_wads >= self.borrowed_amount_wads); } } @@ -1139,74 +1222,65 @@ pub fn add_srm_mint(test: &mut ProgramTest) -> TestQuoteMint { } } -pub struct TestDexMarket { +pub struct TestAggregator { pub name: String, pub pubkey: Pubkey, - pub bids_pubkey: Pubkey, - pub asks_pubkey: Pubkey, + pub price: u64, } -impl TestDexMarket { - pub fn setup(test: &mut ProgramTest, market_pair: TestDexMarketPair) -> TestDexMarket { - let (name, pubkey, bids_pubkey, asks_pubkey) = match market_pair { - TestDexMarketPair::SOL_USDC => { - ("sol_usdc", SOL_USDC_MARKET, SOL_USDC_BIDS, SOL_USDC_ASKS) - } - TestDexMarketPair::SRM_USDC => { - ("srm_usdc", SRM_USDC_MARKET, SRM_USDC_BIDS, SRM_USDC_ASKS) - } - }; +pub fn add_aggregator(test: &mut ProgramTest, pair: TestAggregatorPair) -> TestAggregator { + let (name, decimals, price) = match pair { + TestAggregatorPair::SOL_USDC => ("SOL:USDC", 6, 20 * FRACTIONAL_TO_USDC), + TestAggregatorPair::SRM_USDC => ("SRM:USDC", 6, 5 * FRACTIONAL_TO_USDC), + }; - let pubkey = Pubkey::from_str(pubkey).unwrap(); - let bids_pubkey = Pubkey::from_str(bids_pubkey).unwrap(); - let asks_pubkey = Pubkey::from_str(asks_pubkey).unwrap(); + let pubkey = Pubkey::new_unique(); - test.add_account_with_file_data( - pubkey, - u32::MAX as u64, - Pubkey::new(&[0; 32]), - &format!("{}_dex_market.bin", name), - ); + let mut description = [0u8; 32]; + let size = name.len().min(description.len()); + description[0..size].copy_from_slice(&name.as_bytes()[0..size]); - test.add_account_with_file_data( - bids_pubkey, - u32::MAX as u64, - Pubkey::new(&[0; 32]), - &format!("{}_dex_market_bids.bin", name), - ); + let aggregator = Aggregator { + config: AggregatorConfig { + description, + decimals, + ..AggregatorConfig::default() + }, + is_initialized: true, + answer: Answer { + median: price, + created_at: 1, // set to > 0 to initialize + ..Answer::default() + }, + ..Aggregator::default() + }; - test.add_account_with_file_data( - asks_pubkey, - u32::MAX as u64, - Pubkey::new(&[0; 32]), - &format!("{}_dex_market_asks.bin", name), - ); + let mut account = Account::new( + u32::MAX as u64, + borsh_utils::get_packed_len::(), + &spl_token_lending::id(), + ); + let account_info = (&pubkey, false, &mut account).into_account_info(); + aggregator.save(&account_info).unwrap(); + test.add_account(pubkey, account); - Self { - name: name.to_string(), - pubkey, - bids_pubkey, - asks_pubkey, - } + TestAggregator { + name: name.to_string(), + pubkey, + price, } +} +impl TestAggregator { pub async fn add_to_genesis( &self, banks_client: &mut BanksClient, genesis_accounts: &mut GenesisAccounts, ) { - println!("{}_dex_market: {}", self.name, self.pubkey); + println!("{}_aggregator: {}", self.name, self.pubkey); genesis_accounts .fetch_and_insert(banks_client, self.pubkey) .await; - println!("{}_dex_market_bids: {}", self.name, self.bids_pubkey); - genesis_accounts - .fetch_and_insert(banks_client, self.bids_pubkey) - .await; - println!("{}_dex_market_asks: {}", self.name, self.asks_pubkey); - genesis_accounts - .fetch_and_insert(banks_client, self.asks_pubkey) - .await; } } diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index 46b69d0c32e..ef6c2aa4860 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "test-bpf")] + mod helpers; use helpers::*; @@ -10,7 +12,6 @@ use solana_sdk::{ }; use spl_token_lending::{ error::LendingError, instruction::init_lending_market, processor::process_instruction, - state::PROGRAM_VERSION, }; #[tokio::test] @@ -22,15 +23,18 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(10_000); + test.set_bpf_compute_max_units(8_000); let usdc_mint = add_usdc_mint(&mut test); let (mut banks_client, payer, _recent_blockhash) = test.start().await; - let lending_market = TestLendingMarket::init(&mut banks_client, usdc_mint.pubkey, &payer).await; - let lending_market_info = lending_market.get_state(&mut banks_client).await; - assert_eq!(lending_market_info.version, PROGRAM_VERSION); - assert_eq!(lending_market_info.quote_token_mint, usdc_mint.pubkey); + let test_lending_market = + TestLendingMarket::init(&mut banks_client, usdc_mint.pubkey, &payer).await; + + test_lending_market.validate_state(&mut banks_client).await; + + let lending_market = test_lending_market.get_state(&mut banks_client).await; + assert_eq!(lending_market.quote_token_mint, usdc_mint.pubkey); } #[tokio::test] diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index ed28a8ffa13..72e6715fdac 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -11,8 +11,7 @@ use solana_sdk::{ transaction::{Transaction, TransactionError}, }; use spl_token_lending::{ - error::LendingError, instruction::init_obligation, math::Decimal, - processor::process_instruction, + error::LendingError, instruction::init_obligation, processor::process_instruction, }; #[tokio::test] @@ -24,54 +23,23 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(28_000); + test.set_bpf_compute_max_units(8_000); let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - let (mut banks_client, payer, _recent_blockhash) = test.start().await; let obligation = TestObligation::init( &mut banks_client, &lending_market, - &sol_reserve, - &usdc_reserve, - &payer, &user_accounts_owner, + &payer, ) .await .unwrap(); obligation.validate_state(&mut banks_client).await; - let obligation_token_balance = - get_token_balance(&mut banks_client, obligation.token_account).await; - assert_eq!(obligation_token_balance, 0); } #[tokio::test] @@ -86,45 +54,14 @@ async fn test_already_initialized() { test.set_bpf_compute_max_units(13_000); let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: TEST_RESERVE_CONFIG, - ..AddReserveArgs::default() - }, - ); - let usdc_obligation = add_obligation( &mut test, - &user_accounts_owner, &lending_market, - AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: 0, - borrowed_liquidity_wads: Decimal::zero(), - }, + &user_accounts_owner, + AddObligationArgs::default(), ); let (mut banks_client, payer, recent_blockhash) = test.start().await; @@ -132,14 +69,12 @@ async fn test_already_initialized() { &[init_obligation( spl_token_lending::id(), usdc_obligation.pubkey, - usdc_obligation.token_mint, - usdc_obligation.token_account, - user_accounts_owner.pubkey(), lending_market.pubkey, + user_accounts_owner.pubkey(), )], Some(&payer.pubkey()), ); - transaction.sign(&[&payer], recent_blockhash); + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); assert_eq!( banks_client diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 5d280bff35e..df6ca6e07ee 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -26,12 +26,13 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(70_000); + test.set_bpf_compute_max_units(61_000); let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + let sol_usdc_aggregator = add_aggregator(&mut test, TestAggregatorPair::SOL_USDC); + let (mut banks_client, payer, _recent_blockhash) = test.start().await; const RESERVE_AMOUNT: u64 = 42; @@ -56,7 +57,7 @@ async fn test_success() { sol_user_liquidity_account, &payer, &user_accounts_owner, - &sol_usdc_dex_market, + Some(&sol_usdc_aggregator), ) .await .unwrap(); @@ -64,16 +65,16 @@ async fn test_success() { sol_reserve.validate_state(&mut banks_client).await; let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply).await; + get_token_balance(&mut banks_client, sol_reserve.liquidity_supply_pubkey).await; assert_eq!(sol_liquidity_supply, RESERVE_AMOUNT); let user_sol_balance = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; + get_token_balance(&mut banks_client, sol_reserve.user_liquidity_pubkey).await; assert_eq!(user_sol_balance, 0); let user_sol_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; + get_token_balance(&mut banks_client, sol_reserve.user_collateral_pubkey).await; assert_eq!( user_sol_collateral_balance, - INITIAL_COLLATERAL_RATIO * RESERVE_AMOUNT + RESERVE_AMOUNT * INITIAL_COLLATERAL_RATIO ); } @@ -87,16 +88,14 @@ async fn test_already_initialized() { let user_accounts_owner = Keypair::new(); let user_transfer_authority = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let usdc_reserve = add_reserve( + let usdc_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), liquidity_amount: 42, liquidity_mint_decimals: usdc_mint.decimals, liquidity_mint_pubkey: usdc_mint.pubkey, @@ -111,19 +110,19 @@ async fn test_already_initialized() { &[init_reserve( spl_token_lending::id(), 42, - usdc_reserve.config, - usdc_reserve.user_liquidity_account, - usdc_reserve.user_collateral_account, - usdc_reserve.pubkey, - usdc_reserve.liquidity_mint, - usdc_reserve.liquidity_supply, - usdc_reserve.collateral_mint, - usdc_reserve.collateral_supply, - usdc_reserve.liquidity_fee_receiver, + usdc_test_reserve.config, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.user_collateral_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_mint_pubkey, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + usdc_test_reserve.collateral_mint_pubkey, + usdc_test_reserve.collateral_supply_pubkey, lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority.pubkey(), - Some(sol_usdc_dex_market.pubkey), + None, )], Some(&payer.pubkey()), ); @@ -153,9 +152,10 @@ async fn test_invalid_fees() { ); let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + let sol_usdc_aggregator = add_aggregator(&mut test, TestAggregatorPair::SOL_USDC); + let (mut banks_client, payer, _recent_blockhash) = test.start().await; const RESERVE_AMOUNT: u64 = 42; @@ -189,7 +189,7 @@ async fn test_invalid_fees() { sol_user_liquidity_account, &payer, &user_accounts_owner, - &sol_usdc_dex_market, + Some(&sol_usdc_aggregator) ) .await .unwrap_err(), @@ -219,7 +219,7 @@ async fn test_invalid_fees() { sol_user_liquidity_account, &payer, &user_accounts_owner, - &sol_usdc_dex_market, + Some(&sol_usdc_aggregator) ) .await .unwrap_err(), diff --git a/token-lending/program/tests/liquidate.rs b/token-lending/program/tests/liquidate.rs deleted file mode 100644 index 815db9b7580..00000000000 --- a/token-lending/program/tests/liquidate.rs +++ /dev/null @@ -1,162 +0,0 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_program_test::*; -use solana_sdk::{pubkey::Pubkey, signature::Keypair}; -use spl_token_lending::{ - math::Decimal, processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, -}; - -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - -const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; -const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "spl_token_lending", - spl_token_lending::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_bpf_compute_max_units(97_000); - - // set loan values to about 90% of collateral value so that it gets liquidated - // assumes SOL is ~$14 - const USDC_LOAN: u64 = 12 * FRACTIONAL_TO_USDC; - const USDC_LOAN_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATIO * LAMPORTS_TO_SOL; - - const SOL_LOAN: u64 = LAMPORTS_TO_SOL; - const SOL_LOAN_USDC_COLLATERAL: u64 = 12 * INITIAL_COLLATERAL_RATIO * FRACTIONAL_TO_USDC; - - let user_accounts_owner = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); - let usdc_mint = add_usdc_mint(&mut test); - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - // Loans are unhealthy if borrow is more than 80% of collateral - let mut reserve_config = TEST_RESERVE_CONFIG; - reserve_config.liquidation_threshold = 80; - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - config: reserve_config, - initial_borrow_rate: 1, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: USDC_LOAN * 101 / 100, - user_liquidity_amount: USDC_LOAN, - collateral_amount: SOL_LOAN_USDC_COLLATERAL, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - config: reserve_config, - initial_borrow_rate: 1, - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - collateral_amount: USDC_LOAN_SOL_COLLATERAL, - borrow_amount: SOL_LOAN * 101 / 100, - user_liquidity_amount: SOL_LOAN, - ..AddReserveArgs::default() - }, - ); - - let usdc_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: USDC_LOAN_SOL_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(USDC_LOAN), - }, - ); - - let sol_obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &sol_reserve, - collateral_reserve: &usdc_reserve, - collateral_amount: SOL_LOAN_USDC_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(SOL_LOAN), - }, - ); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - - lending_market - .liquidate( - &mut banks_client, - &payer, - LiquidateArgs { - repay_reserve: &usdc_reserve, - withdraw_reserve: &sol_reserve, - dex_market: &sol_usdc_dex_market, - amount: USDC_LOAN, - user_accounts_owner: &user_accounts_owner, - obligation: &usdc_obligation, - }, - ) - .await; - - lending_market - .liquidate( - &mut banks_client, - &payer, - LiquidateArgs { - repay_reserve: &sol_reserve, - withdraw_reserve: &usdc_reserve, - dex_market: &sol_usdc_dex_market, - amount: SOL_LOAN, - user_accounts_owner: &user_accounts_owner, - obligation: &sol_obligation, - }, - ) - .await; - - let usdc_liquidity_supply = - get_token_balance(&mut banks_client, usdc_reserve.liquidity_supply).await; - let usdc_loan_state = usdc_obligation.get_state(&mut banks_client).await; - let usdc_liquidated = usdc_liquidity_supply - INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL; - assert!(usdc_liquidated > USDC_LOAN / 2); - assert_eq!( - usdc_liquidated, - usdc_loan_state - .borrowed_liquidity_wads - .try_floor_u64() - .unwrap() - ); - - let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply).await; - let sol_loan_state = sol_obligation.get_state(&mut banks_client).await; - let sol_liquidated = sol_liquidity_supply - INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS; - assert!(sol_liquidated > SOL_LOAN / 2); - assert_eq!( - sol_liquidated, - sol_loan_state - .borrowed_liquidity_wads - .try_floor_u64() - .unwrap() - ); -} diff --git a/token-lending/program/tests/liquidate_obligation.rs b/token-lending/program/tests/liquidate_obligation.rs new file mode 100644 index 00000000000..77441a76d13 --- /dev/null +++ b/token-lending/program/tests/liquidate_obligation.rs @@ -0,0 +1,182 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::instruction::approve; +use spl_token_lending::instruction::{liquidate_obligation, refresh_obligation}; +use spl_token_lending::{ + instruction::repay_obligation_liquidity, processor::process_instruction, + state::INITIAL_COLLATERAL_RATIO, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(51_000); + + // 100 SOL collateral + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + // 100 SOL * 80% LTV -> 80 SOL * 20 USDC -> 1600 USDC borrow + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_600 * FRACTIONAL_TO_USDC; + // 1600 USDC * 50% -> 800 USDC liquidation + const USDC_LIQUIDATION_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT_FRACTIONAL / 2; + // 800 USDC / 20 USDC per SOL -> 40 SOL + 10% bonus -> 44 SOL + const SOL_LIQUIDATION_AMOUNT_LAMPORTS: u64 = 44 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + reserve_config.liquidation_threshold = 80; + reserve_config.liquidation_bonus = 10; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + let initial_liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + let initial_user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + let initial_collateral_supply_balance = + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + + let mut transaction = Transaction::new_with_payer( + &[ + approve( + &spl_token::id(), + &usdc_test_reserve.user_liquidity_pubkey, + &user_transfer_authority.pubkey(), + &user_accounts_owner.pubkey(), + &[], + USDC_LIQUIDATION_AMOUNT_FRACTIONAL, + ) + .unwrap(), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), + liquidate_obligation( + spl_token_lending::id(), + USDC_LIQUIDATION_AMOUNT_FRACTIONAL, + usdc_test_reserve.user_liquidity_pubkey, + sol_test_reserve.user_collateral_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.pubkey, + sol_test_reserve.collateral_supply_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + user_transfer_authority.pubkey(), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign( + &[&payer, &user_accounts_owner, &user_transfer_authority], + recent_blockhash, + ); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + + let user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + assert_eq!( + user_liquidity_balance, + initial_user_liquidity_balance - USDC_LIQUIDATION_AMOUNT_FRACTIONAL + ); + + let liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply_balance, + initial_liquidity_supply_balance + USDC_LIQUIDATION_AMOUNT_FRACTIONAL + ); + + let user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + assert_eq!( + user_collateral_balance, + initial_user_collateral_balance + SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + + let collateral_supply_balance = + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + assert_eq!( + collateral_supply_balance, + initial_collateral_supply_balance - SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + + let obligation = test_obligation.get_state(&mut banks_client).await; + assert_eq!( + obligation.deposits[0].deposited_amount, + SOL_DEPOSIT_AMOUNT_LAMPORTS - SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + assert_eq!( + obligation.borrows[0].borrowed_amount_wads, + (USDC_BORROW_AMOUNT_FRACTIONAL - USDC_LIQUIDATION_AMOUNT_FRACTIONAL).into() + ) +} diff --git a/token-lending/program/tests/withdraw.rs b/token-lending/program/tests/redeem_reserve_collateral.rs similarity index 64% rename from token-lending/program/tests/withdraw.rs rename to token-lending/program/tests/redeem_reserve_collateral.rs index 0cf66829e74..3f1363c2816 100644 --- a/token-lending/program/tests/withdraw.rs +++ b/token-lending/program/tests/redeem_reserve_collateral.rs @@ -11,13 +11,10 @@ use solana_sdk::{ }; use spl_token::instruction::approve; use spl_token_lending::{ - instruction::withdraw_reserve_liquidity, processor::process_instruction, + instruction::redeem_reserve_collateral, processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, }; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; -const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 10 * FRACTIONAL_TO_USDC; - #[tokio::test] async fn test_success() { let mut test = ProgramTest::new( @@ -27,25 +24,26 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(33_000); + test.set_bpf_compute_max_units(29_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - const WITHDRAW_COLLATERAL_AMOUNT: u64 = - INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 10 * FRACTIONAL_TO_USDC; + const COLLATERAL_AMOUNT: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL * INITIAL_COLLATERAL_RATIO; - let usdc_reserve = add_reserve( + let usdc_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, + collateral_amount: COLLATERAL_AMOUNT, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, liquidity_mint_decimals: usdc_mint.decimals, liquidity_mint_pubkey: usdc_mint.pubkey, - collateral_amount: WITHDRAW_COLLATERAL_AMOUNT, config: TEST_RESERVE_CONFIG, + mark_fresh: true, ..AddReserveArgs::default() }, ); @@ -57,23 +55,22 @@ async fn test_success() { &[ approve( &spl_token::id(), - &usdc_reserve.user_collateral_account, + &usdc_test_reserve.user_collateral_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - WITHDRAW_COLLATERAL_AMOUNT, + COLLATERAL_AMOUNT, ) .unwrap(), - withdraw_reserve_liquidity( + redeem_reserve_collateral( spl_token_lending::id(), - WITHDRAW_COLLATERAL_AMOUNT, - usdc_reserve.user_collateral_account, - usdc_reserve.user_liquidity_account, - usdc_reserve.pubkey, - usdc_reserve.collateral_mint, - usdc_reserve.liquidity_supply, + COLLATERAL_AMOUNT, + usdc_test_reserve.user_collateral_pubkey, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.collateral_mint_pubkey, + usdc_test_reserve.liquidity_supply_pubkey, lending_market.pubkey, - lending_market.authority, user_transfer_authority.pubkey(), ), ], diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs new file mode 100644 index 00000000000..307df7c23cb --- /dev/null +++ b/token-lending/program/tests/refresh_obligation.rs @@ -0,0 +1,164 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token_lending::math::{Rate, TryAdd, TryMul}; +use spl_token_lending::state::SLOTS_PER_YEAR; +use spl_token_lending::{ + instruction::{refresh_obligation, refresh_reserve}, + math::{Decimal, TryDiv}, + processor::process_instruction, + state::INITIAL_COLLATERAL_RATIO, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(28_000); + + const SOL_DEPOSIT_AMOUNT: u64 = 100; + const USDC_BORROW_AMOUNT: u64 = 1_000; + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = + SOL_DEPOSIT_AMOUNT * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT * FRACTIONAL_TO_USDC; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + // Configure reserve to a fixed borrow rate of 1% + const BORROW_RATE: u8 = 1; + reserve_config.min_borrow_rate = BORROW_RATE; + reserve_config.optimal_borrow_rate = BORROW_RATE; + reserve_config.optimal_utilization_rate = 100; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_decimals: 9, + liquidity_mint_pubkey: spl_token::native_mint::id(), + config: reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_decimals: usdc_mint.decimals, + liquidity_mint_pubkey: usdc_mint.pubkey, + config: reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddObligationArgs::default() + }, + ); + + let mut test_context = test.start_with_context().await; + test_context.warp_to_slot(3).unwrap(); // clock.slot = 3 + + let ProgramTestContext { + mut banks_client, + payer, + last_blockhash: recent_blockhash, + .. + } = test_context; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), + refresh_reserve( + spl_token_lending::id(), + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_aggregator_pubkey, + ), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + let obligation = test_obligation.get_state(&mut banks_client).await; + + let collateral = &obligation.deposits[0]; + let liquidity = &obligation.borrows[0]; + + let collateral_price = collateral.market_value.try_div(SOL_DEPOSIT_AMOUNT).unwrap(); + + let slot_rate = Rate::from_percent(BORROW_RATE) + .try_div(SLOTS_PER_YEAR) + .unwrap(); + let compound_rate = Rate::one().try_add(slot_rate).unwrap(); + let compound_borrow = Decimal::from(USDC_BORROW_AMOUNT) + .try_mul(compound_rate) + .unwrap(); + let compound_borrow_wads = Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL) + .try_mul(compound_rate) + .unwrap(); + + let liquidity_price = liquidity.market_value.try_div(compound_borrow).unwrap(); + + assert_eq!( + usdc_reserve.liquidity.cumulative_borrow_rate_wads, + liquidity.cumulative_borrow_rate_wads + ); + assert_eq!(liquidity.cumulative_borrow_rate_wads, compound_rate.into()); + assert_eq!( + usdc_reserve.liquidity.borrowed_amount_wads, + liquidity.borrowed_amount_wads + ); + assert_eq!(liquidity.borrowed_amount_wads, compound_borrow_wads); + assert_eq!( + Decimal::from(sol_reserve.liquidity.market_price), + collateral_price, + ); + assert_eq!( + Decimal::from(usdc_reserve.liquidity.market_price), + liquidity_price, + ); +} diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs new file mode 100644 index 00000000000..c59fe29c54e --- /dev/null +++ b/token-lending/program/tests/refresh_reserve.rs @@ -0,0 +1,132 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token_lending::{ + instruction::refresh_reserve, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul}, + processor::process_instruction, + state::SLOTS_PER_YEAR, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(16_000); + + const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; + const BORROW_AMOUNT: u64 = 100; + + let user_accounts_owner = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 80; + + // Configure reserve to a fixed borrow rate of 1% + const BORROW_RATE: u8 = 1; + reserve_config.min_borrow_rate = BORROW_RATE; + reserve_config.optimal_borrow_rate = BORROW_RATE; + reserve_config.optimal_utilization_rate = 100; + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: BORROW_AMOUNT, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_decimals: usdc_mint.decimals, + liquidity_mint_pubkey: usdc_mint.pubkey, + config: reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: BORROW_AMOUNT, + liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, + liquidity_mint_decimals: 9, + liquidity_mint_pubkey: spl_token::native_mint::id(), + config: reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let mut test_context = test.start_with_context().await; + test_context.warp_to_slot(3).unwrap(); // clock.slot = 3 + + let ProgramTestContext { + mut banks_client, + payer, + last_blockhash: recent_blockhash, + .. + } = test_context; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), + refresh_reserve( + spl_token_lending::id(), + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_aggregator_pubkey, + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + + let slot_rate = Rate::from_percent(BORROW_RATE) + .try_div(SLOTS_PER_YEAR) + .unwrap(); + let compound_rate = Rate::one().try_add(slot_rate).unwrap(); + let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + + assert_eq!( + sol_reserve.liquidity.cumulative_borrow_rate_wads, + compound_rate.into() + ); + assert_eq!( + sol_reserve.liquidity.cumulative_borrow_rate_wads, + usdc_reserve.liquidity.cumulative_borrow_rate_wads + ); + assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); + assert_eq!( + sol_reserve.liquidity.borrowed_amount_wads, + usdc_reserve.liquidity.borrowed_amount_wads + ); + assert_eq!( + sol_reserve.liquidity.market_price, + sol_test_reserve.market_price + ); + assert_eq!( + usdc_reserve.liquidity.market_price, + usdc_test_reserve.market_price + ); +} diff --git a/token-lending/program/tests/repay.rs b/token-lending/program/tests/repay.rs deleted file mode 100644 index def32709d76..00000000000 --- a/token-lending/program/tests/repay.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use spl_token::instruction::approve; -use spl_token_lending::{ - instruction::repay_obligation_liquidity, - math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, - processor::process_instruction, - state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, -}; - -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "spl_token_lending", - spl_token_lending::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_bpf_compute_max_units(51_000); - - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - - const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC; - const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - - let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); - let usdc_mint = add_usdc_mint(&mut test); - let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - - let usdc_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - config: TEST_RESERVE_CONFIG, - initial_borrow_rate: 1, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: OBLIGATION_LOAN * 101 / 100, - user_liquidity_amount: OBLIGATION_LOAN, - ..AddReserveArgs::default() - }, - ); - - let sol_reserve = add_reserve( - &mut test, - &user_accounts_owner, - &lending_market, - AddReserveArgs { - config: TEST_RESERVE_CONFIG, - slots_elapsed: SLOTS_PER_YEAR, - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - collateral_amount: OBLIGATION_COLLATERAL, - ..AddReserveArgs::default() - }, - ); - - let obligation = add_obligation( - &mut test, - &user_accounts_owner, - &lending_market, - AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: OBLIGATION_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(OBLIGATION_LOAN), - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; - - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &usdc_reserve.user_liquidity_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - OBLIGATION_LOAN, - ) - .unwrap(), - approve( - &spl_token::id(), - &obligation.token_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - OBLIGATION_COLLATERAL, - ) - .unwrap(), - repay_obligation_liquidity( - spl_token_lending::id(), - OBLIGATION_LOAN, - usdc_reserve.user_liquidity_account, - sol_reserve.user_collateral_account, - usdc_reserve.pubkey, - usdc_reserve.liquidity_supply, - sol_reserve.pubkey, - sol_reserve.collateral_supply, - obligation.pubkey, - obligation.token_mint, - obligation.token_account, - lending_market.pubkey, - lending_market.authority, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let collateral_received = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await - - initial_user_collateral_balance; - assert!(collateral_received > 0); - - let borrow_reserve_state = usdc_reserve.get_state(&mut banks_client).await; - assert!(borrow_reserve_state.cumulative_borrow_rate_wads > Decimal::one()); - - let obligation_state = obligation.get_state(&mut banks_client).await; - assert_eq!( - obligation_state.cumulative_borrow_rate_wads, - borrow_reserve_state.cumulative_borrow_rate_wads - ); - assert_eq!( - obligation_state.borrowed_liquidity_wads, - borrow_reserve_state.liquidity.borrowed_amount_wads - ); - - // use cumulative borrow rate directly since test rate starts at 1.0 - let expected_obligation_interest = obligation_state - .cumulative_borrow_rate_wads - .try_mul(OBLIGATION_LOAN) - .unwrap() - .try_sub(Decimal::from(OBLIGATION_LOAN)) - .unwrap(); - assert_eq!( - obligation_state.borrowed_liquidity_wads, - expected_obligation_interest - ); - - let expected_obligation_total = Decimal::from(OBLIGATION_LOAN) - .try_add(expected_obligation_interest) - .unwrap(); - - let expected_obligation_repaid_percent = Decimal::from(OBLIGATION_LOAN) - .try_div(expected_obligation_total) - .unwrap(); - - let expected_collateral_received = expected_obligation_repaid_percent - .try_mul(OBLIGATION_COLLATERAL) - .unwrap() - .try_floor_u64() - .unwrap(); - assert_eq!(collateral_received, expected_collateral_received); - - let expected_collateral_remaining = OBLIGATION_COLLATERAL - expected_collateral_received; - assert_eq!( - obligation_state.deposited_collateral_tokens, - expected_collateral_remaining - ); -} diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs new file mode 100644 index 00000000000..7a8f7292984 --- /dev/null +++ b/token-lending/program/tests/repay_obligation_liquidity.rs @@ -0,0 +1,145 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::instruction::approve; +use spl_token_lending::instruction::refresh_obligation; +use spl_token_lending::{ + instruction::repay_obligation_liquidity, processor::process_instruction, + state::INITIAL_COLLATERAL_RATIO, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(27_000); + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + let initial_liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + + let mut transaction = Transaction::new_with_payer( + &[ + approve( + &spl_token::id(), + &usdc_test_reserve.user_liquidity_pubkey, + &user_transfer_authority.pubkey(), + &user_accounts_owner.pubkey(), + &[], + USDC_BORROW_AMOUNT_FRACTIONAL, + ) + .unwrap(), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), + repay_obligation_liquidity( + spl_token_lending::id(), + USDC_BORROW_AMOUNT_FRACTIONAL, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.pubkey, + test_obligation.pubkey, + lending_market.pubkey, + user_transfer_authority.pubkey(), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign( + &[&payer, &user_accounts_owner, &user_transfer_authority], + recent_blockhash, + ); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + + let user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + assert_eq!( + user_liquidity_balance, + initial_user_liquidity_balance - USDC_BORROW_AMOUNT_FRACTIONAL + ); + + let liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply_balance, + initial_liquidity_supply_balance + USDC_BORROW_AMOUNT_FRACTIONAL + ); + + let obligation = test_obligation.get_state(&mut banks_client).await; + assert_eq!(obligation.borrows.len(), 0); +} diff --git a/token-lending/program/tests/set_lending_market_owner.rs b/token-lending/program/tests/set_lending_market_owner.rs index 57da91a89c3..3652f293f45 100644 --- a/token-lending/program/tests/set_lending_market_owner.rs +++ b/token-lending/program/tests/set_lending_market_owner.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "test-bpf")] + mod helpers; use helpers::*; @@ -24,7 +26,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(4_000); + test.set_bpf_compute_max_units(2_000); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -61,9 +63,6 @@ async fn test_invalid_owner() { processor!(process_instruction), ); - // limit to track compute unit increase - test.set_bpf_compute_max_units(4_000); - let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); let (mut banks_client, payer, recent_blockhash) = test.start().await; @@ -103,9 +102,6 @@ async fn test_owner_not_signer() { processor!(process_instruction), ); - // limit to track compute unit increase - test.set_bpf_compute_max_units(4_000); - let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); let (mut banks_client, payer, recent_blockhash) = test.start().await; diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 5d11819af83..eebe0e8f711 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -5,7 +5,6 @@ use solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - system_instruction::create_account, transaction::{Transaction, TransactionError}, }; @@ -13,19 +12,15 @@ use helpers::*; use spl_token::instruction::approve; use spl_token_lending::{ error::LendingError, - instruction::withdraw_obligation_collateral, - math::Decimal, + instruction::{refresh_obligation, withdraw_obligation_collateral}, processor::process_instruction, - state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, + state::INITIAL_COLLATERAL_RATIO, }; mod helpers; -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; -const FRACTIONAL_TO_USDC: u64 = 1_000_000; - #[tokio::test] -async fn test_success() { +async fn test_withdraw_base_currency_fixed_amount() { let mut test = ProgramTest::new( "spl_token_lending", spl_token_lending::id(), @@ -33,162 +28,156 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(84_000); - - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; + test.set_bpf_compute_max_units(33_000); - const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC; - const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - - // from Reserve::required_collateral_for_borrow - const REQUIRED_COLLATERAL: u64 = 7_220_474_693; - const WITHDRAW_COLLATERAL: u64 = OBLIGATION_COLLATERAL - REQUIRED_COLLATERAL; + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 200 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const WITHDRAW_AMOUNT: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; let user_accounts_owner = Keypair::new(); - let memory_keypair = Keypair::new(); let user_transfer_authority = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let sol_reserve = add_reserve( + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - slots_elapsed: SLOTS_PER_YEAR, - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_decimals: 9, + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, liquidity_mint_pubkey: spl_token::native_mint::id(), - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - collateral_amount: OBLIGATION_COLLATERAL, - config: TEST_RESERVE_CONFIG, + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, ..AddReserveArgs::default() }, ); - let usdc_reserve = add_reserve( + let usdc_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - initial_borrow_rate: 1, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, liquidity_mint_pubkey: usdc_mint.pubkey, liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: OBLIGATION_LOAN * 101 / 100, - user_liquidity_amount: OBLIGATION_LOAN, - config: TEST_RESERVE_CONFIG, + config: reserve_config, + mark_fresh: true, ..AddReserveArgs::default() }, ); - let obligation = add_obligation( + let test_obligation = add_obligation( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: OBLIGATION_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(OBLIGATION_LOAN), + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() }, ); + let test_collateral = &test_obligation.deposits[0]; + let test_liquidity = &test_obligation.borrows[0]; + let (mut banks_client, payer, recent_blockhash) = test.start().await; + test_obligation.validate_state(&mut banks_client).await; + test_collateral.validate_state(&mut banks_client).await; + test_liquidity.validate_state(&mut banks_client).await; + let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; let initial_obligation_token_balance = - get_token_balance(&mut banks_client, obligation.token_account).await; + get_token_balance(&mut banks_client, test_collateral.token_account).await; let mut transaction = Transaction::new_with_payer( &[ - create_account( - &payer.pubkey(), - &memory_keypair.pubkey(), - 0, - 65548, - &spl_token_lending::id(), - ), approve( &spl_token::id(), - &sol_reserve.user_collateral_account, + &sol_test_reserve.user_collateral_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - OBLIGATION_LOAN, + WITHDRAW_AMOUNT, ) .unwrap(), approve( &spl_token::id(), - &obligation.token_account, + &test_collateral.token_account, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - OBLIGATION_COLLATERAL, + WITHDRAW_AMOUNT, ) .unwrap(), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), withdraw_obligation_collateral( spl_token_lending::id(), - WITHDRAW_COLLATERAL, - sol_reserve.collateral_supply, - sol_reserve.user_collateral_account, - sol_reserve.pubkey, - usdc_reserve.pubkey, - obligation.pubkey, - obligation.token_mint, - obligation.token_account, + WITHDRAW_AMOUNT, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.pubkey, + test_obligation.pubkey, + test_collateral.token_mint, + test_collateral.token_account, lending_market.pubkey, - lending_market.authority, user_transfer_authority.pubkey(), - sol_usdc_dex_market.pubkey, - sol_usdc_dex_market.bids_pubkey, - memory_keypair.pubkey(), ), ], Some(&payer.pubkey()), ); transaction.sign( - &[ - &payer, - &memory_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], + &[&payer, &user_accounts_owner, &user_transfer_authority], recent_blockhash, ); assert!(banks_client.process_transaction(transaction).await.is_ok()); // check that collateral tokens were transferred let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; assert_eq!( collateral_supply_balance, - initial_collateral_supply_balance - WITHDRAW_COLLATERAL + initial_collateral_supply_balance - WITHDRAW_AMOUNT ); let user_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; assert_eq!( user_collateral_balance, - initial_user_collateral_balance + WITHDRAW_COLLATERAL + initial_user_collateral_balance + WITHDRAW_AMOUNT ); // check that obligation tokens were burned let obligation_token_balance = - get_token_balance(&mut banks_client, obligation.token_account).await; + get_token_balance(&mut banks_client, test_collateral.token_account).await; assert_eq!( obligation_token_balance, - initial_obligation_token_balance - WITHDRAW_COLLATERAL + initial_obligation_token_balance - WITHDRAW_AMOUNT + ); + + let obligation = test_obligation.get_state(&mut banks_client).await; + let collateral = &obligation.deposits[0]; + assert_eq!( + collateral.deposited_amount, + SOL_DEPOSIT_AMOUNT_LAMPORTS - WITHDRAW_AMOUNT ); } #[tokio::test] -async fn test_withdraw_below_required() { +async fn test_withdraw_quote_currency_all() { let mut test = ProgramTest::new( "spl_token_lending", spl_token_lending::id(), @@ -196,126 +185,246 @@ async fn test_withdraw_below_required() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(84_000); + test.set_bpf_compute_max_units(28_000); + + const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = + 1_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; + const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; + const WITHDRAW_AMOUNT: u64 = u64::max_value(); + + let user_accounts_owner = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let test_collateral = &test_obligation.deposits[0]; + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + test_obligation.validate_state(&mut banks_client).await; + test_collateral.validate_state(&mut banks_client).await; + + let initial_collateral_supply_balance = get_token_balance( + &mut banks_client, + usdc_test_reserve.collateral_supply_pubkey, + ) + .await; + let initial_user_collateral_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; + let initial_obligation_token_balance = + get_token_balance(&mut banks_client, test_collateral.token_account).await; + + let mut transaction = Transaction::new_with_payer( + &[ + approve( + &spl_token::id(), + &usdc_test_reserve.user_collateral_pubkey, + &user_transfer_authority.pubkey(), + &user_accounts_owner.pubkey(), + &[], + WITHDRAW_AMOUNT, + ) + .unwrap(), + approve( + &spl_token::id(), + &test_collateral.token_account, + &user_transfer_authority.pubkey(), + &user_accounts_owner.pubkey(), + &[], + WITHDRAW_AMOUNT, + ) + .unwrap(), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![usdc_test_reserve.pubkey], + ), + withdraw_obligation_collateral( + spl_token_lending::id(), + WITHDRAW_AMOUNT, + usdc_test_reserve.collateral_supply_pubkey, + usdc_test_reserve.user_collateral_pubkey, + usdc_test_reserve.pubkey, + test_obligation.pubkey, + test_collateral.token_mint, + test_collateral.token_account, + lending_market.pubkey, + user_transfer_authority.pubkey(), + ), + ], + Some(&payer.pubkey()), + ); - const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; + transaction.sign( + &[&payer, &user_accounts_owner, &user_transfer_authority], + recent_blockhash, + ); + assert!(banks_client.process_transaction(transaction).await.is_ok()); - const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC; - const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + // check that collateral tokens were transferred + let collateral_supply_balance = get_token_balance( + &mut banks_client, + usdc_test_reserve.collateral_supply_pubkey, + ) + .await; + assert_eq!( + collateral_supply_balance, + initial_collateral_supply_balance - USDC_DEPOSIT_AMOUNT_FRACTIONAL + ); + let user_collateral_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; + assert_eq!( + user_collateral_balance, + initial_user_collateral_balance + USDC_DEPOSIT_AMOUNT_FRACTIONAL + ); - // from Reserve::required_collateral_for_borrow - const REQUIRED_COLLATERAL: u64 = 7_220_474_693; - const WITHDRAW_COLLATERAL: u64 = OBLIGATION_COLLATERAL - REQUIRED_COLLATERAL + 1; + // check that obligation tokens were burned + let obligation_token_balance = + get_token_balance(&mut banks_client, test_collateral.token_account).await; + assert_eq!( + obligation_token_balance, + initial_obligation_token_balance - USDC_DEPOSIT_AMOUNT_FRACTIONAL + ); + + let obligation = test_obligation.get_state(&mut banks_client).await; + assert_eq!(obligation.deposits.len(), 0); +} + +#[tokio::test] +async fn test_withdraw_too_large() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 200 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const WITHDRAW_AMOUNT: u64 = (100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO) + 1; let user_accounts_owner = Keypair::new(); - let memory_keypair = Keypair::new(); let user_transfer_authority = Keypair::new(); - let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); - let sol_reserve = add_reserve( + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - slots_elapsed: SLOTS_PER_YEAR, - liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, - liquidity_mint_decimals: 9, + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, liquidity_mint_pubkey: spl_token::native_mint::id(), - dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - collateral_amount: OBLIGATION_COLLATERAL, - config: TEST_RESERVE_CONFIG, + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, ..AddReserveArgs::default() }, ); - let usdc_reserve = add_reserve( + let usdc_test_reserve = add_reserve( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddReserveArgs { - initial_borrow_rate: 1, - liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, liquidity_mint_pubkey: usdc_mint.pubkey, liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: OBLIGATION_LOAN * 101 / 100, - user_liquidity_amount: OBLIGATION_LOAN, - config: TEST_RESERVE_CONFIG, + config: reserve_config, + mark_fresh: true, ..AddReserveArgs::default() }, ); - let obligation = add_obligation( + let test_obligation = add_obligation( &mut test, - &user_accounts_owner, &lending_market, + &user_accounts_owner, AddObligationArgs { - borrow_reserve: &usdc_reserve, - collateral_reserve: &sol_reserve, - collateral_amount: OBLIGATION_COLLATERAL, - borrowed_liquidity_wads: Decimal::from(OBLIGATION_LOAN), + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() }, ); + let test_collateral = &test_obligation.deposits[0]; + let (mut banks_client, payer, recent_blockhash) = test.start().await; let mut transaction = Transaction::new_with_payer( &[ - create_account( - &payer.pubkey(), - &memory_keypair.pubkey(), - 0, - 65548, - &spl_token_lending::id(), - ), approve( &spl_token::id(), - &sol_reserve.user_collateral_account, + &sol_test_reserve.user_collateral_pubkey, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - OBLIGATION_LOAN, + WITHDRAW_AMOUNT, ) .unwrap(), approve( &spl_token::id(), - &obligation.token_account, + &test_collateral.token_account, &user_transfer_authority.pubkey(), &user_accounts_owner.pubkey(), &[], - OBLIGATION_COLLATERAL, + WITHDRAW_AMOUNT, ) .unwrap(), + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), withdraw_obligation_collateral( spl_token_lending::id(), - WITHDRAW_COLLATERAL, - sol_reserve.collateral_supply, - sol_reserve.user_collateral_account, - sol_reserve.pubkey, - usdc_reserve.pubkey, - obligation.pubkey, - obligation.token_mint, - obligation.token_account, + WITHDRAW_AMOUNT, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.pubkey, + test_obligation.pubkey, + test_collateral.token_mint, + test_collateral.token_account, lending_market.pubkey, - lending_market.authority, user_transfer_authority.pubkey(), - sol_usdc_dex_market.pubkey, - sol_usdc_dex_market.bids_pubkey, - memory_keypair.pubkey(), ), ], Some(&payer.pubkey()), ); transaction.sign( - &[ - &payer, - &memory_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], + &[&payer, &user_accounts_owner, &user_transfer_authority], recent_blockhash, ); @@ -328,7 +437,7 @@ async fn test_withdraw_below_required() { .unwrap(), TransactionError::InstructionError( 3, - InstructionError::Custom(LendingError::ObligationAtLimit as u32) + InstructionError::Custom(LendingError::WithdrawTooLarge as u32) ) ); } From 36ee3e5d866c8e87bb95d54c939092842dedc8b5 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 6 Apr 2021 15:57:46 -0500 Subject: [PATCH 155/191] new end to end test --- .../program/tests/obligation_end_to_end.rs | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 token-lending/program/tests/obligation_end_to_end.rs diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs new file mode 100644 index 00000000000..8ccd52ae83e --- /dev/null +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -0,0 +1,325 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + account::Account, + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction::create_account, + transaction::Transaction, +}; +use spl_token::{ + instruction::approve, + solana_program::program_pack::Pack, + state::{Account as Token, Mint}, +}; +use spl_token_lending::{ + instruction::{ + borrow_obligation_liquidity, deposit_obligation_collateral, init_obligation, + refresh_obligation, refresh_reserve, repay_obligation_liquidity, + withdraw_obligation_collateral, + }, + math::Decimal, + processor::process_instruction, + state::{Obligation, INITIAL_COLLATERAL_RATIO}, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(41_000); + + const FEE_AMOUNT: u64 = 100; + const HOST_FEE_AMOUNT: u64 = 20; + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = SOL_DEPOSIT_AMOUNT_LAMPORTS; + + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC - FEE_AMOUNT; + const USDC_REPAY_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT_FRACTIONAL + FEE_AMOUNT; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = + USDC_BORROW_AMOUNT_FRACTIONAL + USDC_REPAY_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let user_accounts_owner_pubkey = user_accounts_owner.pubkey(); + + let user_transfer_authority = Keypair::new(); + let user_transfer_authority_pubkey = user_transfer_authority.pubkey(); + + let obligation_keypair = Keypair::new(); + let obligation_pubkey = obligation_keypair.pubkey(); + + let obligation_token_mint_keypair = Keypair::new(); + let obligation_token_mint_pubkey = obligation_token_mint_keypair.pubkey(); + + let obligation_token_account_keypair = Keypair::new(); + let obligation_token_account_pubkey = obligation_token_account_keypair.pubkey(); + + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + user_liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + ..AddReserveArgs::default() + }, + ); + + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &user_accounts_owner, + AddReserveArgs { + user_liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + ..AddReserveArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + let payer_pubkey = payer.pubkey(); + + let initial_collateral_supply_balance = + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + let initial_user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + let initial_liquidity_supply = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + let initial_user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + + let rent = banks_client.get_rent().await.unwrap(); + + let mut transaction = Transaction::new_with_payer( + &[ + // 0 + create_account( + &payer.pubkey(), + &obligation_keypair.pubkey(), + rent.minimum_balance(Obligation::LEN), + Obligation::LEN as u64, + &spl_token_lending::id(), + ), + // 1 + init_obligation( + spl_token_lending::id(), + obligation_pubkey, + lending_market.pubkey, + user_accounts_owner_pubkey, + ), + // 2 + create_account( + &payer_pubkey, + &obligation_token_mint_pubkey, + rent.minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token::id(), + ), + // 3 + create_account( + &payer_pubkey, + &obligation_token_account_pubkey, + rent.minimum_balance(Token::LEN), + Token::LEN as u64, + &spl_token::id(), + ), + // 4 + refresh_reserve( + spl_token_lending::id(), + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_aggregator_pubkey, + ), + // 5 + approve( + &spl_token::id(), + &sol_test_reserve.user_collateral_pubkey, + &user_transfer_authority_pubkey, + &user_accounts_owner_pubkey, + &[], + SOL_DEPOSIT_AMOUNT_LAMPORTS, + ) + .unwrap(), + // 6 + deposit_obligation_collateral( + spl_token_lending::id(), + SOL_DEPOSIT_AMOUNT_LAMPORTS, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.pubkey, + obligation_pubkey, + obligation_token_mint_pubkey, + obligation_token_account_pubkey, + lending_market.pubkey, + user_accounts_owner_pubkey, + user_transfer_authority_pubkey, + ), + // 7 + refresh_obligation( + spl_token_lending::id(), + obligation_pubkey, + vec![sol_test_reserve.pubkey], + ), + // 8 + refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), + // 9 + borrow_obligation_liquidity( + spl_token_lending::id(), + USDC_BORROW_AMOUNT_FRACTIONAL, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + obligation_pubkey, + lending_market.pubkey, + user_accounts_owner_pubkey, + Some(usdc_test_reserve.liquidity_host_pubkey), + ), + // 10 + refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), + // 11 + refresh_obligation( + spl_token_lending::id(), + obligation_pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), + // 12 + approve( + &spl_token::id(), + &usdc_test_reserve.user_liquidity_pubkey, + &user_transfer_authority_pubkey, + &user_accounts_owner_pubkey, + &[], + USDC_REPAY_AMOUNT_FRACTIONAL, + ) + .unwrap(), + // 13 + repay_obligation_liquidity( + spl_token_lending::id(), + USDC_REPAY_AMOUNT_FRACTIONAL, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.pubkey, + obligation_pubkey, + lending_market.pubkey, + user_transfer_authority_pubkey, + ), + // 14 + refresh_obligation( + spl_token_lending::id(), + obligation_pubkey, + vec![sol_test_reserve.pubkey], + ), + // 15 + approve( + &spl_token::id(), + &obligation_token_account_pubkey, + &user_transfer_authority_pubkey, + &user_accounts_owner_pubkey, + &[], + SOL_DEPOSIT_AMOUNT_LAMPORTS, + ) + .unwrap(), + // 16 + withdraw_obligation_collateral( + spl_token_lending::id(), + SOL_DEPOSIT_AMOUNT_LAMPORTS, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.pubkey, + obligation_pubkey, + obligation_token_mint_pubkey, + obligation_token_account_pubkey, + lending_market.pubkey, + user_transfer_authority_pubkey, + ), + ], + Some(&payer_pubkey), + ); + + transaction.sign( + &vec![ + &payer, + &obligation_keypair, + &user_accounts_owner, + &obligation_token_mint_keypair, + &obligation_token_account_keypair, + &user_transfer_authority, + ], + recent_blockhash, + ); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + + let obligation = { + let obligation_account: Account = banks_client + .get_account(obligation_pubkey) + .await + .unwrap() + .unwrap(); + Obligation::unpack(&obligation_account.data[..]).unwrap() + }; + + let collateral_supply_balance = + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + let user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + assert_eq!(collateral_supply_balance, initial_collateral_supply_balance); + assert_eq!(user_collateral_balance, initial_user_collateral_balance); + + let liquidity_supply = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + let user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + assert_eq!(liquidity_supply, initial_liquidity_supply); + assert_eq!( + user_liquidity_balance, + initial_user_liquidity_balance - FEE_AMOUNT + ); + assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, Decimal::zero()); + assert_eq!( + usdc_reserve.liquidity.available_amount, + initial_liquidity_supply + ); + + let obligation_token_balance = + get_token_balance(&mut banks_client, obligation_token_account_pubkey).await; + assert_eq!(obligation_token_balance, 0); + assert_eq!(obligation.deposits.len(), 0); + assert_eq!(obligation.borrows.len(), 0); + + let fee_balance = get_token_balance( + &mut banks_client, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + ) + .await; + assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + + let host_fee_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; + assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); +} From d371cceda5321b46e7c6cd468d03dd71e4f1930b Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 7 Apr 2021 20:08:02 -0500 Subject: [PATCH 156/191] restore original order to make code review easier --- token-lending/program/src/instruction.rs | 544 +++---- token-lending/program/src/processor.rs | 1418 ++++++++--------- .../program/src/state/lending_market.rs | 58 +- token-lending/program/src/state/obligation.rs | 222 +-- token-lending/program/src/state/reserve.rs | 346 ++-- token-lending/program/tests/helpers/mod.rs | 6 +- 6 files changed, 1297 insertions(+), 1297 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 4ac8a77568e..f93e14bcd58 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -30,18 +30,6 @@ pub enum LendingInstruction { owner: Pubkey, }, - // 1 - /// Sets the new owner of a lending market. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Lending market account - /// 1. `[signer]` Current owner - SetLendingMarketOwner { - /// The new owner - new_owner: Pubkey, - }, - // 2 /// Initializes a new lending market reserve. /// @@ -73,17 +61,18 @@ pub enum LendingInstruction { config: ReserveConfig, }, - // 3 - /// Accrue interest and update median quote token price on a reserve. + // 6 + /// Initializes a new lending market obligation. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Reserve account. - /// 1. `[]` Clock sysvar. - /// 2. `[optional]` Reserve liquidity aggregator account. - /// Required if the reserve currency is not the lending market quote - /// currency. - RefreshReserve, + /// 0. `[writable]` Obligation account - uninitialized. + /// 1. `[]` Lending market account. + /// 2. `[signer]` Obligation owner. + /// 3. `[]` Clock sysvar. + /// 4. `[]` Rent sysvar. + /// 5. `[]` Token program id. + InitObligation, // 4 /// Deposit liquidity into a reserve in exchange for collateral. Collateral represents a share @@ -127,80 +116,6 @@ pub enum LendingInstruction { collateral_amount: u64, }, - // 6 - /// Initializes a new lending market obligation. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account - uninitialized. - /// 1. `[]` Lending market account. - /// 2. `[signer]` Obligation owner. - /// 3. `[]` Clock sysvar. - /// 4. `[]` Rent sysvar. - /// 5. `[]` Token program id. - InitObligation, - - // 7 - /// Refresh an obligation's accrued interest and collateral and liquidity prices. Requires - /// refreshed reserves, as all obligation collateral deposit reserves in order, followed by all - /// liquidity borrow reserves in order. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account. - /// 1. `[]` Clock sysvar. - /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. - /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. - RefreshObligation, - - // 8 - /// Deposit collateral to an obligation. Requires a refreshed reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Source collateral token account. - /// Minted by deposit reserve collateral mint. - /// $authority can transfer $collateral_amount. - /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. - /// 2. `[]` Deposit reserve account - refreshed. - /// 3. `[writable]` Obligation account. - /// 4. `[writable]` Obligation token mint. - /// 5. `[writable]` Obligation token output account. - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` Obligation owner. - /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Clock sysvar. - /// 11 `[]` Rent sysvar. - /// 12 `[]` Token program id. - DepositObligationCollateral { - /// Amount of collateral to deposit - collateral_amount: u64, - }, - - // 9 - /// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. - /// 1. `[writable]` Destination collateral token account. - /// Minted by withdraw reserve collateral mint. - /// $authority can transfer $collateral_amount. - /// 2. `[]` Withdraw reserve account - refreshed. - /// 3. `[writable]` Obligation account - refreshed. - /// 4. `[writable]` Obligation token mint. - /// 5. `[writable]` Obligation token input account. - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` User transfer authority ($authority). - /// 9. `[]` Clock sysvar. - /// 10 `[]` Token program id. - WithdrawObligationCollateral { - /// Amount of collateral to withdraw - u64::max_value() for up to 100% of deposited amount - collateral_amount: u64, - }, - // 10 /// Borrow liquidity from a reserve by depositing collateral tokens. Requires a refreshed /// obligation and reserve. @@ -272,6 +187,91 @@ pub enum LendingInstruction { /// Amount of liquidity to repay - u64::max_value() for up to 100% of borrowed amount liquidity_amount: u64, }, + + // 3 + /// Accrue interest and update median quote token price on a reserve. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Reserve account. + /// 1. `[]` Clock sysvar. + /// 2. `[optional]` Reserve liquidity aggregator account. + /// Required if the reserve currency is not the lending market quote + /// currency. + RefreshReserve, + + // 8 + /// Deposit collateral to an obligation. Requires a refreshed reserve. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source collateral token account. + /// Minted by deposit reserve collateral mint. + /// $authority can transfer $collateral_amount. + /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. + /// 2. `[]` Deposit reserve account - refreshed. + /// 3. `[writable]` Obligation account. + /// 4. `[writable]` Obligation token mint. + /// 5. `[writable]` Obligation token output account. + /// 6. `[]` Lending market account. + /// 7. `[]` Derived lending market authority. + /// 8. `[signer]` Obligation owner. + /// 9. `[signer]` User transfer authority ($authority). + /// 10 `[]` Clock sysvar. + /// 11 `[]` Rent sysvar. + /// 12 `[]` Token program id. + DepositObligationCollateral { + /// Amount of collateral to deposit + collateral_amount: u64, + }, + + // 9 + /// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. + /// 1. `[writable]` Destination collateral token account. + /// Minted by withdraw reserve collateral mint. + /// $authority can transfer $collateral_amount. + /// 2. `[]` Withdraw reserve account - refreshed. + /// 3. `[writable]` Obligation account - refreshed. + /// 4. `[writable]` Obligation token mint. + /// 5. `[writable]` Obligation token input account. + /// 6. `[]` Lending market account. + /// 7. `[]` Derived lending market authority. + /// 8. `[signer]` User transfer authority ($authority). + /// 9. `[]` Clock sysvar. + /// 10 `[]` Token program id. + WithdrawObligationCollateral { + /// Amount of collateral to withdraw - u64::max_value() for up to 100% of deposited amount + collateral_amount: u64, + }, + + // 1 + /// Sets the new owner of a lending market. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Lending market account + /// 1. `[signer]` Current owner + SetLendingMarketOwner { + /// The new owner + new_owner: Pubkey, + }, + + // 7 + /// Refresh an obligation's accrued interest and collateral and liquidity prices. Requires + /// refreshed reserves, as all obligation collateral deposit reserves in order, followed by all + /// liquidity borrow reserves in order. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation account. + /// 1. `[]` Clock sysvar. + /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. + /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. + RefreshObligation, } impl LendingInstruction { @@ -285,31 +285,27 @@ impl LendingInstruction { let (owner, _rest) = Self::unpack_pubkey(rest)?; Self::InitLendingMarket { owner } } - 1 => { - let (new_owner, _rest) = Self::unpack_pubkey(rest)?; - Self::SetLendingMarketOwner { new_owner } - } 2 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; + let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; + let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; + let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; - let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; - let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; - let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?; let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { optimal_utilization_rate, + loan_to_value_ratio, + liquidation_bonus, + liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, - loan_to_value_ratio, - liquidation_threshold, - liquidation_bonus, fees: ReserveFees { borrow_fee_wad, host_fee_percentage, @@ -317,7 +313,7 @@ impl LendingInstruction { }, } } - 3 => Self::RefreshReserve, + 6 => Self::InitObligation, 4 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::DepositReserveLiquidity { liquidity_amount } @@ -326,16 +322,6 @@ impl LendingInstruction { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; Self::RedeemReserveCollateral { collateral_amount } } - 6 => Self::InitObligation, - 7 => Self::RefreshObligation, - 8 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::DepositObligationCollateral { collateral_amount } - } - 9 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::WithdrawObligationCollateral { collateral_amount } - } 10 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::BorrowObligationLiquidity { liquidity_amount } @@ -348,6 +334,20 @@ impl LendingInstruction { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::LiquidateObligation { liquidity_amount } } + 3 => Self::RefreshReserve, + 8 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::DepositObligationCollateral { collateral_amount } + } + 9 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::WithdrawObligationCollateral { collateral_amount } + } + 1 => { + let (new_owner, _rest) = Self::unpack_pubkey(rest)?; + Self::SetLendingMarketOwner { new_owner } + } + 7 => Self::RefreshObligation, _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -401,21 +401,17 @@ impl LendingInstruction { buf.push(0); buf.extend_from_slice(owner.as_ref()); } - Self::SetLendingMarketOwner { new_owner } => { - buf.push(1); - buf.extend_from_slice(new_owner.as_ref()); - } Self::InitReserve { liquidity_amount, config: ReserveConfig { optimal_utilization_rate, + loan_to_value_ratio, + liquidation_bonus, + liquidation_threshold, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, - loan_to_value_ratio, - liquidation_threshold, - liquidation_bonus, fees: ReserveFees { borrow_fee_wad, @@ -426,17 +422,17 @@ impl LendingInstruction { buf.push(2); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); buf.extend_from_slice(&optimal_utilization_rate.to_le_bytes()); + buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); + buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); + buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); buf.extend_from_slice(&min_borrow_rate.to_le_bytes()); buf.extend_from_slice(&optimal_borrow_rate.to_le_bytes()); buf.extend_from_slice(&max_borrow_rate.to_le_bytes()); - buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes()); - buf.extend_from_slice(&liquidation_threshold.to_le_bytes()); - buf.extend_from_slice(&liquidation_bonus.to_le_bytes()); buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } - Self::RefreshReserve => { - buf.push(3); + Self::InitObligation => { + buf.push(6); } Self::DepositReserveLiquidity { liquidity_amount } => { buf.push(4); @@ -446,11 +442,20 @@ impl LendingInstruction { buf.push(5); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::InitObligation => { - buf.push(6); + Self::BorrowObligationLiquidity { liquidity_amount } => { + buf.push(10); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::RefreshObligation => { - buf.push(7); + Self::RepayObligationLiquidity { liquidity_amount } => { + buf.push(11); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + } + Self::LiquidateObligation { liquidity_amount } => { + buf.push(12); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + } + Self::RefreshReserve => { + buf.push(3); } Self::DepositObligationCollateral { collateral_amount } => { buf.push(8); @@ -460,17 +465,12 @@ impl LendingInstruction { buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::BorrowObligationLiquidity { liquidity_amount } => { - buf.push(10); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - } - Self::RepayObligationLiquidity { liquidity_amount } => { - buf.push(11); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + Self::SetLendingMarketOwner { new_owner } => { + buf.push(1); + buf.extend_from_slice(new_owner.as_ref()); } - Self::LiquidateObligation { liquidity_amount } => { - buf.push(12); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + Self::RefreshObligation => { + buf.push(7); } } buf @@ -499,23 +499,6 @@ pub fn init_lending_market( } } -/// Creates a 'SetLendingMarketOwner' instruction. -pub fn set_lending_market_owner( - program_id: Pubkey, - lending_market_pubkey: Pubkey, - lending_market_owner: Pubkey, - new_owner: Pubkey, -) -> Instruction { - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_owner, true), - ], - data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), - } -} - /// Creates an 'InitReserve' instruction. #[allow(clippy::too_many_arguments)] pub fn init_reserve( @@ -573,6 +556,28 @@ pub fn init_reserve( } } +/// Creates an 'InitObligation' instruction. +#[allow(clippy::too_many_arguments)] +pub fn init_obligation( + program_id: Pubkey, + obligation_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::InitObligation.pack(), + } +} + /// Creates a 'DepositReserveLiquidity' instruction. #[allow(clippy::too_many_arguments)] pub fn deposit_reserve_liquidity( @@ -643,71 +648,133 @@ pub fn redeem_reserve_collateral( } } -/// Creates a `RefreshReserve` instruction -pub fn refresh_reserve( +/// Creates a 'BorrowObligationLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn borrow_obligation_liquidity( program_id: Pubkey, - reserve_pubkey: Pubkey, - reserve_liquidity_aggregator_pubkey: Option, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + borrow_reserve_pubkey: Pubkey, + borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, + obligation_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, + host_fee_receiver_pubkey: Option, ) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); let mut accounts = vec![ - AccountMeta::new(reserve_pubkey, false), + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(borrow_reserve_pubkey, false), + AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), ]; - if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { - accounts.push(AccountMeta::new_readonly( - reserve_liquidity_aggregator_pubkey, - false, - )); + if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { + accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); } Instruction { program_id, accounts, - data: LendingInstruction::RefreshReserve.pack(), + data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), } } -/// Creates an 'InitObligation' instruction. +/// Creates a `RepayObligationLiquidity` instruction #[allow(clippy::too_many_arguments)] -pub fn init_obligation( +pub fn repay_obligation_liquidity( program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, - obligation_owner_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, ) -> Instruction { Instruction { program_id, accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::InitObligation.pack(), + data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), } } -/// Creates a 'RefreshObligation' instruction. +/// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] -pub fn refresh_obligation( +pub fn liquidate_obligation( program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_collateral_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + repay_reserve_liquidity_supply_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - reserve_pubkeys: Vec, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), + } +} + +/// Creates a `RefreshReserve` instruction +pub fn refresh_reserve( + program_id: Pubkey, + reserve_pubkey: Pubkey, + reserve_liquidity_aggregator_pubkey: Option, ) -> Instruction { let mut accounts = vec![ - AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(reserve_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; - accounts.extend( - reserve_pubkeys - .into_iter() - .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), - ); + if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + accounts.push(AccountMeta::new_readonly( + reserve_liquidity_aggregator_pubkey, + false, + )); + } Instruction { program_id, accounts, - data: LendingInstruction::RefreshObligation.pack(), + data: LendingInstruction::RefreshReserve.pack(), } } @@ -788,109 +855,42 @@ pub fn withdraw_obligation_collateral( } } -/// Creates a 'BorrowObligationLiquidity' instruction. -#[allow(clippy::too_many_arguments)] -pub fn borrow_obligation_liquidity( - program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, - obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - obligation_owner_pubkey: Pubkey, - host_fee_receiver_pubkey: Option, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, - ); - let mut accounts = vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(borrow_reserve_pubkey, false), - AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ]; - if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { - accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); - } - Instruction { - program_id, - accounts, - data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), - } -} - -/// Creates a `RepayObligationLiquidity` instruction -#[allow(clippy::too_many_arguments)] -pub fn repay_obligation_liquidity( +/// Creates a 'SetLendingMarketOwner' instruction. +pub fn set_lending_market_owner( program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, - obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, + lending_market_owner: Pubkey, + new_owner: Pubkey, ) -> Instruction { Instruction { program_id, accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_owner, true), ], - data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), + data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), } } -/// Creates a `LiquidateObligation` instruction +/// Creates a 'RefreshObligation' instruction. #[allow(clippy::too_many_arguments)] -pub fn liquidate_obligation( +pub fn refresh_obligation( program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_collateral_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, - repay_reserve_liquidity_supply_pubkey: Pubkey, - withdraw_reserve_pubkey: Pubkey, - withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, + reserve_pubkeys: Vec, ) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, + let mut accounts = vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + accounts.extend( + reserve_pubkeys + .into_iter() + .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), ); Instruction { program_id, - accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), - AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new(withdraw_reserve_pubkey, false), - AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), + accounts, + data: LendingInstruction::RefreshObligation.pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e1226cb707d..4e04f289055 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -40,10 +40,6 @@ pub fn process_instruction( msg!("Instruction: Init Lending Market"); process_init_lending_market(program_id, owner, accounts) } - LendingInstruction::SetLendingMarketOwner { new_owner } => { - msg!("Instruction: Set Lending Market Owner"); - process_set_lending_market_owner(program_id, new_owner, accounts) - } LendingInstruction::InitReserve { liquidity_amount, config, @@ -51,9 +47,9 @@ pub fn process_instruction( msg!("Instruction: Init Reserve"); process_init_reserve(program_id, liquidity_amount, config, accounts) } - LendingInstruction::RefreshReserve => { - msg!("Instruction: Refresh Reserve"); - process_refresh_reserve(program_id, accounts) + LendingInstruction::InitObligation => { + msg!("Instruction: Init Obligation"); + process_init_obligation(program_id, accounts) } LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { msg!("Instruction: Deposit Reserve Liquidity"); @@ -63,22 +59,6 @@ pub fn process_instruction( msg!("Instruction: Redeem Reserve Collateral"); process_redeem_reserve_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::InitObligation => { - msg!("Instruction: Init Obligation"); - process_init_obligation(program_id, accounts) - } - LendingInstruction::RefreshObligation => { - msg!("Instruction: Refresh Obligation"); - process_refresh_obligation(program_id, accounts) - } - LendingInstruction::DepositObligationCollateral { collateral_amount } => { - msg!("Instruction: Deposit Obligation Collateral"); - process_deposit_obligation_collateral(program_id, collateral_amount, accounts) - } - LendingInstruction::WithdrawObligationCollateral { collateral_amount } => { - msg!("Instruction: Withdraw Obligation Collateral"); - process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) - } LendingInstruction::BorrowObligationLiquidity { liquidity_amount } => { msg!("Instruction: Borrow Obligation Liquidity"); process_borrow_obligation_liquidity(program_id, liquidity_amount, accounts) @@ -91,6 +71,26 @@ pub fn process_instruction( msg!("Instruction: Liquidate Obligation"); process_liquidate_obligation(program_id, liquidity_amount, accounts) } + LendingInstruction::RefreshReserve => { + msg!("Instruction: Refresh Reserve"); + process_refresh_reserve(program_id, accounts) + } + LendingInstruction::DepositObligationCollateral { collateral_amount } => { + msg!("Instruction: Deposit Obligation Collateral"); + process_deposit_obligation_collateral(program_id, collateral_amount, accounts) + } + LendingInstruction::WithdrawObligationCollateral { collateral_amount } => { + msg!("Instruction: Withdraw Obligation Collateral"); + process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) + } + LendingInstruction::SetLendingMarketOwner { new_owner } => { + msg!("Instruction: Set Lending Market Owner"); + process_set_lending_market_owner(program_id, new_owner, accounts) + } + LendingInstruction::RefreshObligation => { + msg!("Instruction: Refresh Obligation"); + process_refresh_obligation(program_id, accounts) + } } } @@ -129,36 +129,6 @@ fn process_init_lending_market( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_set_lending_market_owner( - program_id: &Pubkey, - new_owner: Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_owner_info = next_account_info(account_info_iter)?; - - let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - msg!("Lending market provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.owner != lending_market_owner_info.key { - msg!("Lending market owner does not match the lending market owner provided"); - return Err(LendingError::InvalidMarketOwner.into()); - } - if !lending_market_owner_info.is_signer { - msg!("Lending market owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); - } - - lending_market.owner = new_owner; - LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; - - Ok(()) -} - fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, @@ -173,26 +143,26 @@ fn process_init_reserve( msg!("Optimal utilization rate must be in range [0, 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.loan_to_value_ratio >= 100 { msg!("Loan to value ratio must be in range [0, 100)"); return Err(LendingError::InvalidConfig.into()); } + if config.liquidation_bonus > 100 { + msg!("Liquidation bonus must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } if config.liquidation_threshold <= config.loan_to_value_ratio || config.liquidation_threshold > 100 { msg!("Liquidation threshold must be in range (LTV, 100]"); return Err(LendingError::InvalidConfig.into()); } - if config.liquidation_bonus > 100 { - msg!("Liquidation bonus must be in range [0, 100]"); + if config.optimal_borrow_rate < config.min_borrow_rate { + msg!("Optimal borrow rate must be >= min borrow rate"); + return Err(LendingError::InvalidConfig.into()); + } + if config.optimal_borrow_rate > config.max_borrow_rate { + msg!("Optimal borrow rate must be <= max borrow rate"); return Err(LendingError::InvalidConfig.into()); } if config.fees.borrow_fee_wad >= WAD { @@ -369,35 +339,46 @@ fn process_init_reserve( Ok(()) } -fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter().peekable(); - let reserve_info = next_account_info(account_info_iter)?; +#[inline(never)] // avoid stack frame limit +fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - msg!("Reserve provided is not owned by the lending program"); + assert_rent_exempt(rent, obligation_info)?; + let mut obligation = assert_uninitialized::(obligation_info)?; + if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { - let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; - if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { - msg!( - "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" - ); - return Err(LendingError::InvalidAccountInput.into()); - } + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } - reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; - } else if account_info_iter.peek().is_some() { - msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); - return Err(LendingError::InvalidAccountInput.into()); + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); } - reserve.accrue_interest(clock.slot)?; - reserve.last_update.update_slot(clock.slot); - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + obligation.init(InitObligationParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, + owner: *obligation_owner_info.key, + deposits: Vec::with_capacity(0), + borrows: Vec::with_capacity(0), + }); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } @@ -603,22 +584,28 @@ fn process_redeem_reserve_collateral( } #[inline(never)] // avoid stack frame limit -fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { +fn process_borrow_obligation_liquidity( + program_id: &Pubkey, + liquidity_amount: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); + return Err(LendingError::InvalidAmount.into()); + } + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let borrow_reserve_info = next_account_info(account_info_iter)?; + let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, obligation_info)?; - let mut obligation = assert_uninitialized::(obligation_info)?; - if obligation_info.owner != program_id { - msg!("Obligation provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); @@ -629,174 +616,164 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidTokenProgram.into()); } - if !obligation_owner_info.is_signer { - msg!("Obligation owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); + let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve_info.owner != program_id { + msg!("Borrow reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &borrow_reserve.lending_market != lending_market_info.key { + msg!("Borrow reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { + msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + msg!( + "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } + if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { + msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!("Borrow reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); } - - obligation.init(InitObligationParams { - current_slot: clock.slot, - lending_market: *lending_market_info.key, - owner: *obligation_owner_info.key, - deposits: Vec::with_capacity(0), - borrows: Vec::with_capacity(0), - }); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - - Ok(()) -} - -fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter().peekable(); - let obligation_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } + if &obligation.lending_market != lending_market_info.key { + msg!("Obligation lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); + return Err(LendingError::ObligationStale.into()); + } + if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); + return Err(LendingError::InvalidObligationOwner.into()); + } + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } + if obligation.deposits.is_empty() { + msg!("Obligation has no deposits to borrow against"); + return Err(LendingError::ObligationDepositsEmpty.into()); + } + if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposits have zero value"); + return Err(LendingError::ObligationDepositsZero.into()); + } - let mut deposited_value = Decimal::zero(); - let mut borrowed_value = Decimal::zero(); - let mut loan_to_value_ratio = Decimal::zero(); - let mut liquidation_threshold = Decimal::zero(); - - for (index, collateral) in obligation.deposits.iter_mut().enumerate() { - let deposit_reserve_info = next_account_info(account_info_iter)?; - if deposit_reserve_info.owner != program_id { - msg!( - "Deposit reserve provided for collateral {} is not owned by the lending program", - index - ); - return Err(LendingError::InvalidAccountOwner.into()); - } - if collateral.deposit_reserve != *deposit_reserve_info.key { - msg!( - "Deposit reserve of collateral {} does not match the deposit reserve provided", - index - ); - return Err(LendingError::InvalidAccountInput.into()); - } - - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve.last_update.is_stale(clock.slot)? { - msg!( - "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", - index - ); - return Err(LendingError::ReserveStale.into()); - } - - let decimals = 10u64 - .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); + } - let market_value = deposit_reserve - .collateral_exchange_rate()? - .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? - .try_mul(deposit_reserve.liquidity.market_price)? - .try_div(decimals)?; - collateral.market_value = market_value; + let max_borrow_value = obligation.max_borrow_value()?; + if max_borrow_value == Decimal::zero() { + msg!("Maximum borrow value is zero"); + return Err(LendingError::BorrowTooLarge.into()); + } - let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); - let liquidation_threshold_rate = - Rate::from_percent(deposit_reserve.config.liquidation_threshold); + let BorrowLiquidityResult { + borrow_amount, + receive_amount, + borrow_fee, + host_fee, + } = borrow_reserve.borrow_liquidity(liquidity_amount, max_borrow_value)?; - deposited_value = deposited_value.try_add(market_value)?; - loan_to_value_ratio = - loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; - liquidation_threshold = - liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; + if receive_amount == 0 { + msg!("Borrow amount is too small to receive liquidity after fees"); + return Err(LendingError::BorrowTooSmall.into()); } - for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { - let borrow_reserve_info = next_account_info(account_info_iter)?; - if borrow_reserve_info.owner != program_id { - msg!( - "Borrow reserve provided for liquidity {} is not owned by the lending program", - index - ); - return Err(LendingError::InvalidAccountOwner.into()); - } - if liquidity.borrow_reserve != *borrow_reserve_info.key { - msg!( - "Borrow reserve of liquidity {} does not match the borrow reserve provided", - index - ); - return Err(LendingError::InvalidAccountInput.into()); - } - - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve.last_update.is_stale(clock.slot)? { - msg!( - "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", - index - ); - return Err(LendingError::ReserveStale.into()); - } + borrow_reserve.liquidity.borrow(borrow_amount)?; + borrow_reserve.last_update.mark_stale(); + Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - let decimals = 10u64 - .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; + obligation + .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key)? + .borrow(borrow_amount)?; + obligation.last_update.mark_stale(); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; - let market_value = liquidity - .borrowed_amount_wads - .try_mul(borrow_reserve.liquidity.market_price)? - .try_div(decimals)?; - liquidity.market_value = market_value; - borrowed_value = borrowed_value.try_add(market_value)?; - } + let mut owner_fee = borrow_fee; + if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { + if host_fee > 0 { + owner_fee = owner_fee + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; - if account_info_iter.peek().is_some() { - msg!("Too many obligation deposit or borrow reserves provided"); - return Err(LendingError::InvalidAccountInput.into()); + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: host_fee_receiver_info.clone(), + amount: host_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + } } - - obligation.deposited_value = deposited_value; - obligation.borrowed_value = borrowed_value; - - if deposited_value == Decimal::zero() { - obligation.loan_to_value_ratio = Rate::zero(); - obligation.liquidation_threshold = Rate::zero(); - } else { - obligation.loan_to_value_ratio = - Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; - obligation.liquidation_threshold = - Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; + if owner_fee > 0 { + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: borrow_reserve_liquidity_fee_receiver_info.clone(), + amount: owner_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; } - obligation.last_update.update_slot(clock.slot); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: receive_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; Ok(()) } #[inline(never)] // avoid stack frame limit -fn process_deposit_obligation_collateral( +fn process_repay_obligation_liquidity( program_id: &Pubkey, - collateral_amount: u64, + liquidity_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if collateral_amount == 0 { - msg!("Collateral amount provided cannot be zero"); + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_collateral_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent_info = next_account_info(account_info_iter)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -809,33 +786,27 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidTokenProgram.into()); } - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - msg!("Deposit reserve provided is not owned by the lending program"); + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Deposit reserve lending market does not match the lending market provided"); + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { - msg!( - "Deposit reserve collateral supply must be used as the destination collateral provided" - ); + if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { + msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } - if deposit_reserve.last_update.is_stale(clock.slot)? { - msg!("Deposit reserve is stale and must be refreshed in the current slot"); + if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } - if deposit_reserve.config.loan_to_value_ratio == 0 { - msg!("Deposit reserve has collateral disabled for borrowing"); - return Err(LendingError::ReserveCollateralDisabled.into()); - } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -846,124 +817,67 @@ fn process_deposit_obligation_collateral( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation.owner != obligation_owner_info.key { - msg!("Obligation owner does not match the obligation owner provided"); - return Err(LendingError::InvalidObligationOwner.into()); + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); + return Err(LendingError::ObligationStale.into()); } - if !obligation_owner_info.is_signer { - msg!("Obligation owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); + + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + if liquidity.borrowed_amount_wads == Decimal::zero() { + msg!("Liquidity borrowed amount is zero"); + return Err(LendingError::ObligationLiquidityEmpty.into()); } - // @TODO: Does there need to be a check to make sure obligation_token_mint_info is rent exempt? - let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint.is_initialized() { - if obligation_token_mint_info.owner != token_program_id.key { - msg!("Obligation token mint provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) - { - msg!("Obligation token mint authority does not match the lending market authority provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - } else { - spl_token_init_mint(TokenInitializeMintParams { - mint: obligation_token_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: deposit_reserve.liquidity.mint_decimals, - token_program: token_program_id.clone(), - })?; + let RepayLiquidityResult { + settle_amount, + repay_amount, + } = repay_reserve.repay_liquidity(liquidity_amount, liquidity.borrowed_amount_wads)?; + + if repay_amount == 0 { + msg!("Repay amount is too small to transfer liquidity"); + return Err(LendingError::RepayTooSmall.into()); } - // @TODO: Does there need to be a check to make sure obligation_token_output_info is rent exempt? - let obligation_token_output = - Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; - if obligation_token_output.is_initialized() { - if obligation_token_output_info.owner != token_program_id.key { - msg!("Obligation token output provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation_token_output.mint != obligation_token_mint_info.key { - msg!("Obligation token output mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } - if &obligation_token_output.owner != obligation_owner_info.key { - msg!("Obligation token output owner does not match the obligation owner provided"); - return Err(LendingError::InvalidObligationOwner.into()); - } - } else { - spl_token_init_account(TokenInitializeAccountParams { - account: obligation_token_output_info.clone(), - mint: obligation_token_mint_info.clone(), - owner: obligation_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - } - - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if &lending_market_authority_pubkey != lending_market_authority_info.key { - msg!( - "Derived lending market authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidMarketAuthority.into()); - } + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - obligation - .find_or_add_collateral_to_deposits( - *deposit_reserve_info.key, - *obligation_token_mint_info.key, - )? - .deposit(collateral_amount)?; + obligation.repay(settle_amount, liquidity_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), - destination: destination_collateral_info.clone(), - amount: collateral_amount, + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; - spl_token_mint_to(TokenMintToParams { - mint: obligation_token_mint_info.clone(), - destination: obligation_token_output_info.clone(), - amount: collateral_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - Ok(()) } #[inline(never)] // avoid stack frame limit -fn process_withdraw_obligation_collateral( +fn process_liquidate_obligation( program_id: &Pubkey, - collateral_amount: u64, + liquidity_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if collateral_amount == 0 { - msg!("Collateral amount provided cannot be zero"); + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_collateral_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; + let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; + let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_input_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -980,6 +894,34 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidTokenProgram.into()); } + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Repay reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { + msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!( + "Repay reserve collateral supply cannot be used as the destination collateral provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } + if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); + } + let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { msg!("Withdraw reserve provided is not owned by the lending program"); @@ -989,8 +931,12 @@ fn process_withdraw_obligation_collateral( msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { - msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); + if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { + msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { @@ -1015,39 +961,23 @@ fn process_withdraw_obligation_collateral( msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } - - let (collateral, collateral_index) = - obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - if &collateral.token_mint != obligation_token_mint_info.key { - msg!("Collateral token mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } - if collateral.deposited_amount == 0 { - msg!("Collateral deposited amount is zero"); - return Err(LendingError::ObligationCollateralEmpty.into()); + if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposited value is zero"); + return Err(LendingError::ObligationDepositsZero.into()); } - - let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint_info.owner != token_program_id.key { - msg!("Obligation token mint provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); + if obligation.borrowed_value == Decimal::zero() { + msg!("Obligation borrowed value is zero"); + return Err(LendingError::ObligationBorrowsZero.into()); } - if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { - msg!( - "Obligation token mint authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidAccountOwner.into()); + if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { + msg!("Obligation is healthy and cannot be liquidated"); + return Err(LendingError::ObligationHealthy.into()); } - let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; - if obligation_token_input_info.owner != token_program_id.key { - msg!("Obligation token input provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation_token_input.mint != obligation_token_mint_info.key { - msg!("Obligation token input mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + let (collateral, collateral_index) = + obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1062,72 +992,46 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } - let withdraw_amount = if obligation.borrows.is_empty() { - // there are no borrows; they have been repaid, liquidated, or were never taken out - collateral.deposited_amount - } else if obligation.borrowed_value == Decimal::zero() { - // there are borrows, but they cannot be valued; they must be repaid to withdraw collateral - msg!("Obligation borrowed value is zero"); - return Err(LendingError::ObligationBorrowsZero.into()); - } else if obligation.deposited_value == Decimal::zero() { - // there are deposits, but they cannot be valued - msg!("Obligation deposited value is zero"); - return Err(LendingError::ObligationDepositsZero.into()); - } else { - // there are borrows and deposits, and they can both be valued - let max_withdraw_value = obligation.max_withdraw_value()?; - if max_withdraw_value == Decimal::zero() { - msg!("Maximum withdraw value is zero"); - return Err(LendingError::WithdrawTooLarge.into()); - } - - let withdraw_amount = if collateral_amount == u64::max_value() { - let withdraw_value = max_withdraw_value.min(collateral.market_value); - let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; - withdraw_pct - .try_mul(collateral.deposited_amount)? - .try_floor_u64()? - .min(collateral.deposited_amount) - } else { - let withdraw_amount = collateral_amount.min(collateral.deposited_amount); - let withdraw_pct = - Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - msg!("Withdraw value cannot exceed maximum withdraw value"); - return Err(LendingError::WithdrawTooLarge.into()); - } - withdraw_amount - }; - if withdraw_amount == 0 { - msg!("Withdraw amount is too small to transfer collateral"); - return Err(LendingError::WithdrawTooSmall.into()); - } - withdraw_amount - }; + let LiquidateObligationResult { + settle_amount, + repay_amount, + withdraw_amount, + } = withdraw_reserve.liquidate_obligation( + liquidity_amount, + &obligation, + &liquidity, + &collateral, + )?; - let obligation_token_amount = collateral - .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; - if obligation_token_amount == 0 { - msg!("Withdraw amount is too small to burn obligation tokens"); - return Err(LendingError::WithdrawTooSmall.into()); + if repay_amount == 0 { + msg!("Liquidation is too small to transfer liquidity"); + return Err(LendingError::LiquidationTooSmall.into()); + } + if withdraw_amount == 0 { + msg!("Liquidation is too small to receive collateral"); + return Err(LendingError::LiquidationTooSmall.into()); } + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + + obligation.repay(settle_amount, liquidity_index)?; obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - spl_token_burn(TokenBurnParams { - mint: obligation_token_mint_info.clone(), - source: obligation_token_input_info.clone(), - amount: obligation_token_amount, + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: repay_reserve_liquidity_supply_info.clone(), + amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), + source: withdraw_reserve_collateral_supply_info.clone(), destination: destination_collateral_info.clone(), amount: withdraw_amount, authority: lending_market_authority_info.clone(), @@ -1138,66 +1042,102 @@ fn process_withdraw_obligation_collateral( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_borrow_obligation_liquidity( - program_id: &Pubkey, - liquidity_amount: u64, - accounts: &[AccountInfo], -) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); - return Err(LendingError::InvalidAmount.into()); - } - - let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; - let obligation_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let obligation_owner_info = next_account_info(account_info_iter)?; +fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - msg!("Lending market provided is not owned by the lending program"); + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &lending_market.token_program_id != token_program_id.key { - msg!("Lending market token program does not match the token program provided"); + + if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { + let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; + if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { + msg!( + "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } + + reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; + } else if account_info_iter.peek().is_some() { + msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); + return Err(LendingError::InvalidAccountInput.into()); + } + + reserve.accrue_interest(clock.slot)?; + reserve.last_update.update_slot(clock.slot); + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + + Ok(()) +} + +#[inline(never)] // avoid stack frame limit +fn process_deposit_obligation_collateral( + program_id: &Pubkey, + collateral_amount: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); + return Err(LendingError::InvalidAmount.into()); + } + + let account_info_iter = &mut accounts.iter(); + let source_collateral_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; + let obligation_info = next_account_info(account_info_iter)?; + let obligation_token_mint_info = next_account_info(account_info_iter)?; + let obligation_token_output_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent_info = next_account_info(account_info_iter)?; + let token_program_id = next_account_info(account_info_iter)?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } - let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - msg!("Borrow reserve provided is not owned by the lending program"); + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { + msg!("Deposit reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Borrow reserve lending market does not match the lending market provided"); + if &deposit_reserve.lending_market != lending_market_info.key { + msg!("Deposit reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); + if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { + msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { msg!( - "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" + "Deposit reserve collateral supply must be used as the destination collateral provided" ); return Err(LendingError::InvalidAccountInput.into()); } - if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { - msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if borrow_reserve.last_update.is_stale(clock.slot)? { - msg!("Borrow reserve is stale and must be refreshed in the current slot"); + if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!("Deposit reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } + if deposit_reserve.config.loan_to_value_ratio == 0 { + msg!("Deposit reserve has collateral disabled for borrowing"); + return Err(LendingError::ReserveCollateralDisabled.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -1208,10 +1148,6 @@ fn process_borrow_obligation_liquidity( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.last_update.is_stale(clock.slot)? { - msg!("Obligation is stale and must be refreshed in the current slot"); - return Err(LendingError::ObligationStale.into()); - } if &obligation.owner != obligation_owner_info.key { msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); @@ -1220,13 +1156,53 @@ fn process_borrow_obligation_liquidity( msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } - if obligation.deposits.is_empty() { - msg!("Obligation has no deposits to borrow against"); - return Err(LendingError::ObligationDepositsEmpty.into()); + + // @TODO: Does there need to be a check to make sure obligation_token_mint_info is rent exempt? + let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint.is_initialized() { + if obligation_token_mint_info.owner != token_program_id.key { + msg!("Obligation token mint provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) + { + msg!("Obligation token mint authority does not match the lending market authority provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + } else { + spl_token_init_mint(TokenInitializeMintParams { + mint: obligation_token_mint_info.clone(), + authority: lending_market_authority_info.key, + rent: rent_info.clone(), + decimals: deposit_reserve.liquidity.mint_decimals, + token_program: token_program_id.clone(), + })?; } - if obligation.deposited_value == Decimal::zero() { - msg!("Obligation deposits have zero value"); - return Err(LendingError::ObligationDepositsZero.into()); + + // @TODO: Does there need to be a check to make sure obligation_token_output_info is rent exempt? + let obligation_token_output = + Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; + if obligation_token_output.is_initialized() { + if obligation_token_output_info.owner != token_program_id.key { + msg!("Obligation token output provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if &obligation_token_output.mint != obligation_token_mint_info.key { + msg!("Obligation token output mint does not match the obligation token mint provided"); + return Err(LendingError::InvalidTokenMint.into()); + } + if &obligation_token_output.owner != obligation_owner_info.key { + msg!("Obligation token output owner does not match the obligation owner provided"); + return Err(LendingError::InvalidObligationOwner.into()); + } + } else { + spl_token_init_account(TokenInitializeAccountParams { + account: obligation_token_output_info.clone(), + mint: obligation_token_mint_info.clone(), + owner: obligation_owner_info.clone(), + rent: rent_info.clone(), + token_program: token_program_id.clone(), + })?; } let authority_signer_seeds = &[ @@ -1242,66 +1218,28 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let max_borrow_value = obligation.max_borrow_value()?; - if max_borrow_value == Decimal::zero() { - msg!("Maximum borrow value is zero"); - return Err(LendingError::BorrowTooLarge.into()); - } - - let BorrowLiquidityResult { - borrow_amount, - receive_amount, - borrow_fee, - host_fee, - } = borrow_reserve.borrow_liquidity(liquidity_amount, max_borrow_value)?; - - if receive_amount == 0 { - msg!("Borrow amount is too small to receive liquidity after fees"); - return Err(LendingError::BorrowTooSmall.into()); - } - - borrow_reserve.liquidity.borrow(borrow_amount)?; - borrow_reserve.last_update.mark_stale(); - Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; - obligation - .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key)? - .borrow(borrow_amount)?; + .find_or_add_collateral_to_deposits( + *deposit_reserve_info.key, + *obligation_token_mint_info.key, + )? + .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - let mut owner_fee = borrow_fee; - if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { - if host_fee > 0 { - owner_fee = owner_fee - .checked_sub(host_fee) - .ok_or(LendingError::MathOverflow)?; - - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: host_fee_receiver_info.clone(), - amount: host_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - } - } - if owner_fee > 0 { - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: borrow_reserve_liquidity_fee_receiver_info.clone(), - amount: owner_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - } - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: receive_amount, + source: source_collateral_info.clone(), + destination: destination_collateral_info.clone(), + amount: collateral_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + + spl_token_mint_to(TokenMintToParams { + mint: obligation_token_mint_info.clone(), + destination: obligation_token_output_info.clone(), + amount: collateral_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), @@ -1311,22 +1249,25 @@ fn process_borrow_obligation_liquidity( } #[inline(never)] // avoid stack frame limit -fn process_repay_obligation_liquidity( +fn process_withdraw_obligation_collateral( program_id: &Pubkey, - liquidity_amount: u64, + collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); + if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; + let source_collateral_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let withdraw_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; + let obligation_token_mint_info = next_account_info(account_info_iter)?; + let obligation_token_input_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1341,25 +1282,25 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { - msg!("Repay reserve provided is not owned by the lending program"); + let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; + if withdraw_reserve_info.owner != program_id { + msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.lending_market != lending_market_info.key { - msg!("Repay reserve lending market does not match the lending market provided"); + if &withdraw_reserve.lending_market != lending_market_info.key { + msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); + if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { + msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { - msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); + if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if repay_reserve.last_update.is_stale(clock.slot)? { - msg!("Repay reserve is stale and must be refreshed in the current slot"); + if withdraw_reserve.last_update.is_stale(clock.slot)? { + msg!("Withdraw reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } @@ -1377,222 +1318,281 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationStale.into()); } - let (liquidity, liquidity_index) = - obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; - if liquidity.borrowed_amount_wads == Decimal::zero() { - msg!("Liquidity borrowed amount is zero"); - return Err(LendingError::ObligationLiquidityEmpty.into()); + let (collateral, collateral_index) = + obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + if &collateral.token_mint != obligation_token_mint_info.key { + msg!("Collateral token mint does not match the obligation token mint provided"); + return Err(LendingError::InvalidTokenMint.into()); + } + if collateral.deposited_amount == 0 { + msg!("Collateral deposited amount is zero"); + return Err(LendingError::ObligationCollateralEmpty.into()); } - let RepayLiquidityResult { - settle_amount, - repay_amount, - } = repay_reserve.repay_liquidity(liquidity_amount, liquidity.borrowed_amount_wads)?; + let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; + if obligation_token_mint_info.owner != token_program_id.key { + msg!("Obligation token mint provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { + msg!( + "Obligation token mint authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidAccountOwner.into()); + } - if repay_amount == 0 { - msg!("Repay amount is too small to transfer liquidity"); - return Err(LendingError::RepayTooSmall.into()); + let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; + if obligation_token_input_info.owner != token_program_id.key { + msg!("Obligation token input provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if &obligation_token_input.mint != obligation_token_mint_info.key { + msg!("Obligation token input mint does not match the obligation token mint provided"); + return Err(LendingError::InvalidTokenMint.into()); } - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - repay_reserve.last_update.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); + } - obligation.repay(settle_amount, liquidity_index)?; + let withdraw_amount = if obligation.borrows.is_empty() { + // there are no borrows; they have been repaid, liquidated, or were never taken out + collateral.deposited_amount + } else if obligation.borrowed_value == Decimal::zero() { + // there are borrows, but they cannot be valued; they must be repaid to withdraw collateral + msg!("Obligation borrowed value is zero"); + return Err(LendingError::ObligationBorrowsZero.into()); + } else if obligation.deposited_value == Decimal::zero() { + // there are deposits, but they cannot be valued + msg!("Obligation deposited value is zero"); + return Err(LendingError::ObligationDepositsZero.into()); + } else { + // there are borrows and deposits, and they can both be valued + let max_withdraw_value = obligation.max_withdraw_value()?; + if max_withdraw_value == Decimal::zero() { + msg!("Maximum withdraw value is zero"); + return Err(LendingError::WithdrawTooLarge.into()); + } + + let withdraw_amount = if collateral_amount == u64::max_value() { + let withdraw_value = max_withdraw_value.min(collateral.market_value); + let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; + withdraw_pct + .try_mul(collateral.deposited_amount)? + .try_floor_u64()? + .min(collateral.deposited_amount) + } else { + let withdraw_amount = collateral_amount.min(collateral.deposited_amount); + let withdraw_pct = + Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + msg!("Withdraw value cannot exceed maximum withdraw value"); + return Err(LendingError::WithdrawTooLarge.into()); + } + withdraw_amount + }; + if withdraw_amount == 0 { + msg!("Withdraw amount is too small to transfer collateral"); + return Err(LendingError::WithdrawTooSmall.into()); + } + withdraw_amount + }; + + let obligation_token_amount = collateral + .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; + if obligation_token_amount == 0 { + msg!("Withdraw amount is too small to burn obligation tokens"); + return Err(LendingError::WithdrawTooSmall.into()); + } + + obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: repay_amount, + spl_token_burn(TokenBurnParams { + mint: obligation_token_mint_info.clone(), + source: obligation_token_input_info.clone(), + amount: obligation_token_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; + spl_token_transfer(TokenTransferParams { + source: source_collateral_info.clone(), + destination: destination_collateral_info.clone(), + amount: withdraw_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + Ok(()) } #[inline(never)] // avoid stack frame limit -fn process_liquidate_obligation( +fn process_set_lending_market_owner( program_id: &Pubkey, - liquidity_amount: u64, + new_owner: Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); - return Err(LendingError::InvalidAmount.into()); - } - let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let withdraw_reserve_info = next_account_info(account_info_iter)?; - let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; - let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; + let lending_market_owner_info = next_account_info(account_info_iter)?; - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &lending_market.token_program_id != token_program_id.key { - msg!("Lending market token program does not match the token program provided"); - return Err(LendingError::InvalidTokenProgram.into()); - } - - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { - msg!("Repay reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &repay_reserve.lending_market != lending_market_info.key { - msg!("Repay reserve lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!( - "Repay reserve collateral supply cannot be used as the destination collateral provided" - ); - return Err(LendingError::InvalidAccountInput.into()); + if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); + return Err(LendingError::InvalidMarketOwner.into()); } - if repay_reserve.last_update.is_stale(clock.slot)? { - msg!("Repay reserve is stale and must be refreshed in the current slot"); - return Err(LendingError::ReserveStale.into()); + if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); } - let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; - if withdraw_reserve_info.owner != program_id { - msg!("Withdraw reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &withdraw_reserve.lending_market != lending_market_info.key { - msg!("Withdraw reserve lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if withdraw_reserve.last_update.is_stale(clock.slot)? { - msg!("Withdraw reserve is stale and must be refreshed in the current slot"); - return Err(LendingError::ReserveStale.into()); - } + lending_market.owner = new_owner; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; + + Ok(()) +} + +fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let obligation_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.lending_market != lending_market_info.key { - msg!("Obligation lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if obligation.last_update.is_stale(clock.slot)? { - msg!("Obligation is stale and must be refreshed in the current slot"); - return Err(LendingError::ObligationStale.into()); - } - if obligation.deposited_value == Decimal::zero() { - msg!("Obligation deposited value is zero"); - return Err(LendingError::ObligationDepositsZero.into()); - } - if obligation.borrowed_value == Decimal::zero() { - msg!("Obligation borrowed value is zero"); - return Err(LendingError::ObligationBorrowsZero.into()); - } - if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { - msg!("Obligation is healthy and cannot be liquidated"); - return Err(LendingError::ObligationHealthy.into()); - } - let (liquidity, liquidity_index) = - obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; - let (collateral, collateral_index) = - obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + let mut deposited_value = Decimal::zero(); + let mut borrowed_value = Decimal::zero(); + let mut loan_to_value_ratio = Decimal::zero(); + let mut liquidation_threshold = Decimal::zero(); - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if &lending_market_authority_pubkey != lending_market_authority_info.key { - msg!( - "Derived lending market authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidMarketAuthority.into()); - } + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { + let deposit_reserve_info = next_account_info(account_info_iter)?; + if deposit_reserve_info.owner != program_id { + msg!( + "Deposit reserve provided for collateral {} is not owned by the lending program", + index + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if collateral.deposit_reserve != *deposit_reserve_info.key { + msg!( + "Deposit reserve of collateral {} does not match the deposit reserve provided", + index + ); + return Err(LendingError::InvalidAccountInput.into()); + } - let LiquidateObligationResult { - settle_amount, - repay_amount, - withdraw_amount, - } = withdraw_reserve.liquidate_obligation( - liquidity_amount, - &obligation, - &liquidity, - &collateral, - )?; + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", + index + ); + return Err(LendingError::ReserveStale.into()); + } - if repay_amount == 0 { - msg!("Liquidation is too small to transfer liquidity"); - return Err(LendingError::LiquidationTooSmall.into()); + let decimals = 10u64 + .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + + let market_value = deposit_reserve + .collateral_exchange_rate()? + .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? + .try_mul(deposit_reserve.liquidity.market_price)? + .try_div(decimals)?; + collateral.market_value = market_value; + + let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); + let liquidation_threshold_rate = + Rate::from_percent(deposit_reserve.config.liquidation_threshold); + + deposited_value = deposited_value.try_add(market_value)?; + loan_to_value_ratio = + loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; + liquidation_threshold = + liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } - if withdraw_amount == 0 { - msg!("Liquidation is too small to receive collateral"); - return Err(LendingError::LiquidationTooSmall.into()); + + for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { + let borrow_reserve_info = next_account_info(account_info_iter)?; + if borrow_reserve_info.owner != program_id { + msg!( + "Borrow reserve provided for liquidity {} is not owned by the lending program", + index + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if liquidity.borrow_reserve != *borrow_reserve_info.key { + msg!( + "Borrow reserve of liquidity {} does not match the borrow reserve provided", + index + ); + return Err(LendingError::InvalidAccountInput.into()); + } + + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", + index + ); + return Err(LendingError::ReserveStale.into()); + } + + let decimals = 10u64 + .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + + liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; + let market_value = liquidity + .borrowed_amount_wads + .try_mul(borrow_reserve.liquidity.market_price)? + .try_div(decimals)?; + liquidity.market_value = market_value; + borrowed_value = borrowed_value.try_add(market_value)?; } - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - repay_reserve.last_update.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + if account_info_iter.peek().is_some() { + msg!("Too many obligation deposit or borrow reserves provided"); + return Err(LendingError::InvalidAccountInput.into()); + } - obligation.repay(settle_amount, liquidity_index)?; - obligation.withdraw(withdraw_amount, collateral_index)?; - obligation.last_update.mark_stale(); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + obligation.deposited_value = deposited_value; + obligation.borrowed_value = borrowed_value; - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: repay_reserve_liquidity_supply_info.clone(), - amount: repay_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; + if deposited_value == Decimal::zero() { + obligation.loan_to_value_ratio = Rate::zero(); + obligation.liquidation_threshold = Rate::zero(); + } else { + obligation.loan_to_value_ratio = + Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; + obligation.liquidation_threshold = + Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; + } - spl_token_transfer(TokenTransferParams { - source: withdraw_reserve_collateral_supply_info.clone(), - destination: destination_collateral_info.clone(), - amount: withdraw_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + obligation.last_update.update_slot(clock.slot); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index 5de3807ff7e..b747388f94d 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -14,24 +14,12 @@ pub struct LendingMarket { pub version: u8, /// Bump seed for derived authority address pub bump_seed: u8, - /// Token program id - pub token_program_id: Pubkey, - /// Quote currency token mint - pub quote_token_mint: Pubkey, /// Owner authority which can add new reserves pub owner: Pubkey, -} - -/// Initialize a lending market -pub struct InitLendingMarketParams { - /// Bump seed for derived authority address - pub bump_seed: u8, - /// Token program id - pub token_program_id: Pubkey, /// Quote currency token mint pub quote_token_mint: Pubkey, - /// Owner authority which can add new reserves - pub owner: Pubkey, + /// Token program id + pub token_program_id: Pubkey, } impl LendingMarket { @@ -52,6 +40,18 @@ impl LendingMarket { } } +/// 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, + /// Quote currency token mint + pub quote_token_mint: Pubkey, + /// Token program id + pub token_program_id: Pubkey, +} + impl Sealed for LendingMarket {} impl IsInitialized for LendingMarket { fn is_initialized(&self) -> bool { @@ -65,23 +65,11 @@ const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128 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, token_program_id, quote_token_mint, owner, _padding) = - mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; - *version = self.version.to_le_bytes(); - *bump_seed = self.bump_seed.to_le_bytes(); - token_program_id.copy_from_slice(self.token_program_id.as_ref()); - quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); - owner.copy_from_slice(self.owner.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, token_program_id, quote_token_mint, owner, _padding) = + let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) = array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { @@ -92,9 +80,21 @@ impl Pack for LendingMarket { Ok(Self { version, bump_seed: u8::from_le_bytes(*bump_seed), - token_program_id: Pubkey::new_from_array(*token_program_id), - quote_token_mint: Pubkey::new_from_array(*quote_token_mint), owner: Pubkey::new_from_array(*owner), + quote_token_mint: Pubkey::new_from_array(*quote_token_mint), + token_program_id: Pubkey::new_from_array(*token_program_id), }) } + + 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_token_mint, token_program_id, _padding) = + mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; + *version = self.version.to_le_bytes(); + *bump_seed = self.bump_seed.to_le_bytes(); + owner.copy_from_slice(self.owner.as_ref()); + quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); + token_program_id.copy_from_slice(self.token_program_id.as_ref()); + } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index a899e63c3ce..b8a85c4b1b2 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -42,20 +42,6 @@ pub struct Obligation { pub liquidation_threshold: Rate, } -/// 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 Obligation { /// Create a new obligation pub fn new(params: InitObligationParams) -> Self { @@ -74,15 +60,22 @@ impl Obligation { self.borrows = params.borrows; } - /// 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 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); + let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; + liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct) + } + + /// 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 @@ -96,9 +89,15 @@ impl Obligation { Ok(()) } - /// 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) + /// 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 collateral value that can be withdrawn for a given loan to value ratio @@ -119,19 +118,6 @@ impl Obligation { max_borrowed_value.try_sub(self.borrowed_value) } - /// 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); - 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, @@ -220,6 +206,20 @@ impl Obligation { } } +/// 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 { @@ -346,78 +346,6 @@ const OBLIGATION_LEN: usize = 948; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 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, - loan_to_value_ratio, - liquidation_threshold, - deposits_len, - borrows_len, - data_flat, - ) = mut_array_refs![ - output, - 1, - 8, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 16, - 16, - 16, - 16, - 1, - 1, - OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) - ]; - - *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_rate(self.loan_to_value_ratio, loan_to_value_ratio); - pack_rate(self.liquidation_threshold, liquidation_threshold); - *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; - 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, token_mint, deposited_amount, market_value) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; - deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); - token_mint.copy_from_slice(collateral.token_mint.as_ref()); - *deposited_amount = collateral.deposited_amount.to_le_bytes(); - pack_decimal(collateral.market_value, market_value); - offset += OBLIGATION_COLLATERAL_LEN; - } - 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) = - mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; - 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; - } - } - fn unpack_from_slice(src: &[u8]) -> Result { let input = array_ref![src, 0, OBLIGATION_LEN]; #[allow(clippy::ptr_offset_with_cast)] @@ -504,4 +432,76 @@ impl Pack for Obligation { liquidation_threshold: unpack_rate(liquidation_threshold), }) } + + 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, + loan_to_value_ratio, + liquidation_threshold, + deposits_len, + borrows_len, + data_flat, + ) = mut_array_refs![ + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 16, + 16, + 16, + 16, + 1, + 1, + OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) + ]; + + *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_rate(self.loan_to_value_ratio, loan_to_value_ratio); + pack_rate(self.liquidation_threshold, liquidation_threshold); + *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; + 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, token_mint, deposited_amount, market_value) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; + deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); + token_mint.copy_from_slice(collateral.token_mint.as_ref()); + *deposited_amount = collateral.deposited_amount.to_le_bytes(); + pack_decimal(collateral.market_value, market_value); + offset += OBLIGATION_COLLATERAL_LEN; + } + 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) = + mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; + 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; + } + } } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f30fed4ddae..e3f85019fbf 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -39,20 +39,6 @@ pub struct Reserve { pub config: ReserveConfig, } -/// 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, -} - impl Reserve { /// Create a new reserve pub fn new(params: InitReserveParams) -> Self { @@ -311,6 +297,20 @@ impl Reserve { } } +/// 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, +} + /// Borrow liquidity result #[derive(Debug)] pub struct BorrowLiquidityResult { @@ -368,22 +368,6 @@ pub struct ReserveLiquidity { pub borrowed_amount_wads: Decimal, } -/// 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 fee receiver address - pub fee_receiver: Pubkey, - /// Optional reserve liquidity aggregator state account - pub aggregator: COption, - /// Reserve liquidity market price in quote currency - pub market_price: u64, -} - impl ReserveLiquidity { /// Create a new reserve liquidity pub fn new(params: NewReserveLiquidityParams) -> Self { @@ -484,6 +468,22 @@ impl ReserveLiquidity { } } +/// 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 fee receiver address + pub fee_receiver: Pubkey, + /// Optional reserve liquidity aggregator state account + pub aggregator: COption, + /// Reserve liquidity market price in quote currency + pub market_price: u64, +} + /// Reserve collateral #[derive(Clone, Debug, Default, PartialEq)] pub struct ReserveCollateral { @@ -495,14 +495,6 @@ pub struct ReserveCollateral { pub supply_pubkey: Pubkey, } -/// Create a new reserve collateral -pub struct NewReserveCollateralParams { - /// Reserve collateral mint address - pub mint_pubkey: Pubkey, - /// Reserve collateral supply address - pub supply_pubkey: Pubkey, -} - impl ReserveCollateral { /// Create a new reserve collateral pub fn new(params: NewReserveCollateralParams) -> Self { @@ -547,6 +539,14 @@ impl ReserveCollateral { } } +/// 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); @@ -592,19 +592,19 @@ impl From for Rate { pub struct ReserveConfig { /// Optimal utilization rate, as a percentage pub optimal_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, + /// Bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage + pub liquidation_bonus: u8, + /// Loan to value ratio at which an obligation can be liquidated, as a percentage + pub 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, - /// 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, - /// Loan to value ratio at which an obligation can be liquidated, as a percentage - pub liquidation_threshold: u8, - /// Bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage - pub liquidation_bonus: u8, /// Program owner fees assessed, separate from gains due to interest accrual pub fees: ReserveFees, } @@ -627,14 +627,6 @@ pub struct ReserveFees { pub host_fee_percentage: u8, } -/// 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 ReserveFees { /// Calculate the owner and host fees on borrow pub fn calculate_borrow_fees( @@ -680,6 +672,14 @@ impl ReserveFees { } } +/// 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 { @@ -693,8 +693,9 @@ const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) impl Pack for Reserve { const LEN: usize = RESERVE_LEN; - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN]; + /// 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, @@ -704,123 +705,24 @@ impl Pack for Reserve { liquidity_mint, liquidity_mint_decimals, liquidity_supply, - liquidity_fee_receiver, - liquidity_aggregator, - liquidity_cumulative_borrow_rate_wads, - liquidity_market_price, - liquidity_available_amount, - liquidity_borrowed_amount_wads, collateral_mint, - collateral_mint_supply, collateral_supply, + liquidity_fee_receiver, + liquidity_aggregator, 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_loan_to_value_ratio, - config_liquidation_threshold, - config_liquidation_bonus, config_fees_borrow_fee_wad, config_fees_host_fee_percentage, - _padding, - ) = mut_array_refs![ - output, - 1, - 8, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 4 + PUBKEY_BYTES, - 16, - 8, - 8, - 16, - PUBKEY_BYTES, - 8, - PUBKEY_BYTES, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 8, - 1, - 256 - ]; - *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.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); - *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); - liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); - liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); - pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); - pack_decimal( - self.liquidity.cumulative_borrow_rate_wads, liquidity_cumulative_borrow_rate_wads, - ); - *liquidity_market_price = self.liquidity.market_price.to_le_bytes(); - *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); - pack_decimal( - self.liquidity.borrowed_amount_wads, liquidity_borrowed_amount_wads, - ); - - // collateral - collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); - collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); - - // config - *config_optimal_utilization_rate = self.config.optimal_utilization_rate.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_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes(); - *config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes(); - *config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); - *config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); - *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.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, - liquidity_mint_decimals, - liquidity_supply, - liquidity_fee_receiver, - liquidity_aggregator, - liquidity_cumulative_borrow_rate_wads, - liquidity_market_price, liquidity_available_amount, - liquidity_borrowed_amount_wads, - collateral_mint, collateral_mint_supply, - collateral_supply, - config_optimal_utilization_rate, - config_min_borrow_rate, - config_optimal_borrow_rate, - config_max_borrow_rate, - config_loan_to_value_ratio, - config_liquidation_threshold, - config_liquidation_bonus, - config_fees_borrow_fee_wad, - config_fees_host_fee_percentage, + liquidity_market_price, _padding, ) = array_refs![ input, @@ -832,14 +734,9 @@ impl Pack for Reserve { 1, PUBKEY_BYTES, PUBKEY_BYTES, - 4 + PUBKEY_BYTES, - 16, - 8, - 8, - 16, PUBKEY_BYTES, - 8, PUBKEY_BYTES, + 4 + PUBKEY_BYTES, 1, 1, 1, @@ -849,6 +746,11 @@ impl Pack for Reserve { 1, 8, 1, + 16, + 16, + 8, + 8, + 8, 256 ]; let version = u8::from_le_bytes(*version); @@ -867,12 +769,12 @@ impl Pack for Reserve { mint_pubkey: Pubkey::new_from_array(*liquidity_mint), mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), supply_pubkey: Pubkey::new_from_array(*liquidity_supply), - fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), - aggregator: unpack_coption_key(liquidity_aggregator)?, - cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), - market_price: u64::from_le_bytes(*liquidity_market_price), available_amount: u64::from_le_bytes(*liquidity_available_amount), borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), + aggregator: unpack_coption_key(liquidity_aggregator)?, + market_price: u64::from_le_bytes(*liquidity_market_price), + cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), + fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), }, collateral: ReserveCollateral { mint_pubkey: Pubkey::new_from_array(*collateral_mint), @@ -881,12 +783,12 @@ impl Pack for Reserve { }, config: ReserveConfig { optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate), + loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), + liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus), + liquidation_threshold: u8::from_le_bytes(*config_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: u8::from_le_bytes(*config_max_borrow_rate), - loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), - liquidation_threshold: u8::from_le_bytes(*config_liquidation_threshold), - liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus), fees: ReserveFees { borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), @@ -894,4 +796,102 @@ impl Pack for Reserve { }, }) } + + 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, + liquidity_mint_decimals, + liquidity_supply, + collateral_mint, + collateral_supply, + liquidity_fee_receiver, + liquidity_aggregator, + 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_host_fee_percentage, + liquidity_cumulative_borrow_rate_wads, + liquidity_borrowed_amount_wads, + liquidity_available_amount, + collateral_mint_supply, + liquidity_market_price, + _padding, + ) = mut_array_refs![ + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + 4 + PUBKEY_BYTES, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 8, + 1, + 16, + 16, + 8, + 8, + 8, + 256 + ]; + *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); + pack_decimal( + self.liquidity.cumulative_borrow_rate_wads, + liquidity_cumulative_borrow_rate_wads, + ); + lending_market.copy_from_slice(self.lending_market.as_ref()); + pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); + *liquidity_market_price = self.liquidity.market_price.to_le_bytes(); + + // liquidity info + liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); + *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); + liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); + *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); + pack_decimal( + self.liquidity.borrowed_amount_wads, + liquidity_borrowed_amount_wads, + ); + + // collateral info + collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); + *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); + collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); + liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); + + // config + *config_optimal_utilization_rate = self.config.optimal_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_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); + *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); + } } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index d83c143d716..c9f6f8da7cf 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -46,12 +46,12 @@ pub const FRACTIONAL_TO_SRM: u64 = 1_000_000; pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig { optimal_utilization_rate: 80, + loan_to_value_ratio: 50, + liquidation_bonus: 5, + liquidation_threshold: 55, min_borrow_rate: 0, optimal_borrow_rate: 4, max_borrow_rate: 30, - loan_to_value_ratio: 50, - liquidation_threshold: 55, - liquidation_bonus: 5, fees: ReserveFees { borrow_fee_wad: 100_000_000_000, /// 0.00001% (Aave borrow fee) From f5b2941975fadd6cdbd401c6aa6d648b90820caf Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 13 Apr 2021 13:43:02 -0500 Subject: [PATCH 157/191] addressing review feedback --- token-lending/program/src/instruction.rs | 15 ++-- token-lending/program/src/math/decimal.rs | 2 +- token-lending/program/src/math/rate.rs | 8 +- token-lending/program/src/processor.rs | 27 +++--- token-lending/program/src/state/obligation.rs | 1 + token-lending/program/src/state/reserve.rs | 90 +++++++++---------- .../tests/borrow_obligation_liquidity.rs | 3 +- .../program/tests/genesis_accounts.rs | 4 +- token-lending/program/tests/helpers/mod.rs | 2 +- .../tests/withdraw_obligation_collateral.rs | 10 +-- 10 files changed, 85 insertions(+), 77 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index f93e14bcd58..c3137a1d781 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -92,7 +92,7 @@ pub enum LendingInstruction { /// 8. `[]` Clock sysvar. /// 9. `[]` Token program id. DepositReserveLiquidity { - /// Amount of liquidity to deposit in exchange for collateral + /// Amount of liquidity to deposit in exchange for collateral tokens liquidity_amount: u64, }, @@ -112,10 +112,11 @@ pub enum LendingInstruction { /// 7. `[signer]` User transfer authority ($authority). /// 8. `[]` Token program id. RedeemReserveCollateral { - /// Amount of collateral to redeem in exchange for liquidity + /// Amount of collateral tokens to redeem in exchange for liquidity collateral_amount: u64, }, + // @TODO: rename cf. https://git.io/JOOE6 // 10 /// Borrow liquidity from a reserve by depositing collateral tokens. Requires a refreshed /// obligation and reserve. @@ -136,7 +137,7 @@ pub enum LendingInstruction { /// 9. `[]` Token program id. /// 10. `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { - /// Amount of liquidity to borrow - u64::max_value() for 100% of borrowing power + /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power liquidity_amount: u64, // @TODO: slippage constraint - https://git.io/JmV67 }, @@ -158,7 +159,7 @@ pub enum LendingInstruction { /// 7. `[]` Clock sysvar. /// 8. `[]` Token program id. RepayObligationLiquidity { - /// Amount of liquidity to repay - u64::max_value() for 100% of borrowed amount + /// Amount of liquidity to repay - u64::MAX for 100% of borrowed amount liquidity_amount: u64, }, @@ -184,7 +185,7 @@ pub enum LendingInstruction { /// 10 `[]` Clock sysvar. /// 11 `[]` Token program id. LiquidateObligation { - /// Amount of liquidity to repay - u64::max_value() for up to 100% of borrowed amount + /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount liquidity_amount: u64, }, @@ -221,7 +222,7 @@ pub enum LendingInstruction { /// 11 `[]` Rent sysvar. /// 12 `[]` Token program id. DepositObligationCollateral { - /// Amount of collateral to deposit + /// Amount of collateral tokens to deposit collateral_amount: u64, }, @@ -244,7 +245,7 @@ pub enum LendingInstruction { /// 9. `[]` Clock sysvar. /// 10 `[]` Token program id. WithdrawObligationCollateral { - /// Amount of collateral to withdraw - u64::max_value() for up to 100% of deposited amount + /// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount collateral_amount: u64, }, diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index 1e4f3e60b9e..a6da35bfdc3 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -1,5 +1,5 @@ //! Math for preserving precision of token amounts which are limited -//! by the SPL Token program to be at most u64::max_value(). +//! 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 diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 033d4ce59e6..0661e494efa 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -23,7 +23,11 @@ use crate::{ math::{common::*, decimal::Decimal}, }; use solana_program::program_error::ProgramError; -use std::{convert::TryFrom, fmt}; +use std::{ + convert::TryFrom, + fmt, + u64 +}; use uint::construct_uint; // U128 with 128 bits consisting of 2 x 64-bit words @@ -193,6 +197,6 @@ mod test { #[test] fn checked_pow() { - assert_eq!(Rate::one(), Rate::one().try_pow(u64::max_value()).unwrap()); + assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 4e04f289055..ab81015c4e1 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -5,9 +5,9 @@ use crate::{ instruction::LendingInstruction, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, WAD}, state::{ - BorrowLiquidityResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, - LendingMarket, LiquidateObligationResult, NewReserveCollateralParams, - NewReserveLiquidityParams, Obligation, RepayLiquidityResult, Reserve, ReserveCollateral, + CalculateBorrowResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, + LendingMarket, CalculateLiquidationResult, NewReserveCollateralParams, + NewReserveLiquidityParams, Obligation, CalculateRepayResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, }, }; @@ -26,7 +26,10 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::{Account, Mint}; -use std::convert::TryFrom; +use std::{ + convert::TryFrom, + u64 +}; /// Processes an instruction pub fn process_instruction( @@ -693,19 +696,19 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::BorrowTooLarge.into()); } - let BorrowLiquidityResult { + let CalculateBorrowResult { borrow_amount, receive_amount, borrow_fee, host_fee, - } = borrow_reserve.borrow_liquidity(liquidity_amount, max_borrow_value)?; + } = borrow_reserve.calculate_borrow(liquidity_amount, max_borrow_value)?; if receive_amount == 0 { msg!("Borrow amount is too small to receive liquidity after fees"); return Err(LendingError::BorrowTooSmall.into()); } - borrow_reserve.liquidity.borrow(borrow_amount)?; + borrow_reserve.liquidity.borrow(borrow_amount, receive_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; @@ -829,10 +832,10 @@ fn process_repay_obligation_liquidity( return Err(LendingError::ObligationLiquidityEmpty.into()); } - let RepayLiquidityResult { + let CalculateRepayResult { settle_amount, repay_amount, - } = repay_reserve.repay_liquidity(liquidity_amount, liquidity.borrowed_amount_wads)?; + } = repay_reserve.calculate_repay(liquidity_amount, liquidity.borrowed_amount_wads)?; if repay_amount == 0 { msg!("Repay amount is too small to transfer liquidity"); @@ -992,11 +995,11 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - let LiquidateObligationResult { + let CalculateLiquidationResult { settle_amount, repay_amount, withdraw_amount, - } = withdraw_reserve.liquidate_obligation( + } = withdraw_reserve.calculate_liquidation( liquidity_amount, &obligation, &liquidity, @@ -1383,7 +1386,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::WithdrawTooLarge.into()); } - let withdraw_amount = if collateral_amount == u64::max_value() { + let withdraw_amount = if collateral_amount == u64::MAX { let withdraw_value = max_withdraw_value.min(collateral.market_value); let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; withdraw_pct diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index b8a85c4b1b2..8cf7ed5db44 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -343,6 +343,7 @@ impl ObligationLiquidity { const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 948; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (88 * 1) + (80 * 9) +// @TODO: break this up by obligation / collateral / liquidity impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e3f85019fbf..24ad8f1bee9 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -13,8 +13,11 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; -use std::cmp::Ordering; -use std::convert::{TryFrom, TryInto}; +use std::{ + cmp::Ordering, + convert::{TryFrom, TryInto}, + u64 +}; /// Percentage of an obligation that can be repaid during each liquidation call pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50; @@ -135,15 +138,15 @@ impl Reserve { } /// Borrow liquidity up to a maximum market value - pub fn borrow_liquidity( + pub fn calculate_borrow( &self, - liquidity_amount: u64, + amount_to_borrow: u64, max_borrow_value: Decimal, - ) -> Result { + ) -> Result { let decimals = 10u64 .checked_pow(self.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; - if liquidity_amount == u64::max_value() { + if amount_to_borrow == u64::MAX { let borrow_amount = max_borrow_value .try_mul(decimals)? .try_div(self.liquidity.market_price)? @@ -157,14 +160,14 @@ impl Reserve { .checked_sub(origination_fee) .ok_or(LendingError::MathOverflow)?; - Ok(BorrowLiquidityResult { + Ok(CalculateBorrowResult { borrow_amount, receive_amount, borrow_fee: origination_fee, host_fee, }) } else { - let receive_amount = liquidity_amount; + let receive_amount = amount_to_borrow; let borrow_amount = Decimal::from(receive_amount); let (borrow_fee, host_fee) = self .config @@ -180,7 +183,7 @@ impl Reserve { return Err(LendingError::BorrowTooLarge.into()); } - Ok(BorrowLiquidityResult { + Ok(CalculateBorrowResult { borrow_amount, receive_amount, borrow_fee, @@ -190,42 +193,38 @@ impl Reserve { } /// Repay liquidity up to the borrowed amount - pub fn repay_liquidity( + pub fn calculate_repay( &self, - liquidity_amount: u64, - borrow_amount: Decimal, - ) -> Result { - let settle_amount = if liquidity_amount == u64::max_value() { - borrow_amount + amount_to_repay: u64, + borrowed_amount: Decimal, + ) -> Result { + let settle_amount = if amount_to_repay == u64::MAX { + borrowed_amount } else { - Decimal::from(liquidity_amount).min(borrow_amount) - }; - let repay_amount = if settle_amount == borrow_amount { - settle_amount.try_ceil_u64()? - } else { - settle_amount.try_floor_u64()? + Decimal::from(amount_to_repay).min(borrowed_amount) }; + let repay_amount = settle_amount.try_ceil_u64()?; - Ok(RepayLiquidityResult { + Ok(CalculateRepayResult { settle_amount, repay_amount, }) } /// Liquidate some or all of an unhealthy obligation - pub fn liquidate_obligation( + pub fn calculate_liquidation( &self, - liquidity_amount: u64, + amount_to_liquidate: u64, obligation: &Obligation, liquidity: &ObligationLiquidity, collateral: &ObligationCollateral, - ) -> Result { + ) -> Result { let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; - let target_amount = if liquidity_amount == u64::max_value() { + let max_amount = if amount_to_liquidate == u64::MAX { liquidity.borrowed_amount_wads } else { - Decimal::from(liquidity_amount).min(liquidity.borrowed_amount_wads) + Decimal::from(amount_to_liquidate).min(liquidity.borrowed_amount_wads) }; let settle_amount; @@ -241,26 +240,26 @@ impl Reserve { match liquidation_value.cmp(&collateral.market_value) { Ordering::Greater => { let repay_pct = collateral.market_value.try_div(liquidation_value)?; - repay_amount = target_amount.try_mul(repay_pct)?.try_ceil_u64()?; + repay_amount = max_amount.try_mul(repay_pct)?.try_ceil_u64()?; withdraw_amount = collateral.deposited_amount; } Ordering::Equal => { - repay_amount = target_amount.try_ceil_u64()?; + repay_amount = max_amount.try_ceil_u64()?; withdraw_amount = collateral.deposited_amount; } Ordering::Less => { let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; - repay_amount = target_amount.try_ceil_u64()?; + repay_amount = max_amount.try_floor_u64()?; withdraw_amount = Decimal::from(collateral.deposited_amount) .try_mul(withdraw_pct)? - .try_ceil_u64()?; + .try_floor_u64()?; } } } else { // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded up let liquidation_amount = obligation .max_liquidation_amount(liquidity)? - .min(target_amount); + .min(max_amount); let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?; let liquidation_value = liquidity .market_value @@ -271,25 +270,26 @@ impl Reserve { 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_floor_u64()?; withdraw_amount = Decimal::from(collateral.deposited_amount) .try_mul(withdraw_pct)? - .try_ceil_u64()?; + .try_floor_u64()?; } } - - repay_amount = settle_amount.try_ceil_u64()?; } - Ok(LiquidateObligationResult { + Ok(CalculateLiquidationResult { settle_amount, repay_amount, withdraw_amount, @@ -311,9 +311,9 @@ pub struct InitReserveParams { pub config: ReserveConfig, } -/// Borrow liquidity result +/// Calculate borrow result #[derive(Debug)] -pub struct BorrowLiquidityResult { +pub struct CalculateBorrowResult { /// Total amount of borrow including fees pub borrow_amount: Decimal, /// Borrow amount portion of total amount @@ -324,18 +324,18 @@ pub struct BorrowLiquidityResult { pub host_fee: u64, } -/// Repay liquidity result +/// Calculate repay result #[derive(Debug)] -pub struct RepayLiquidityResult { +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, } -/// Liquidate obligation result +/// Calculate liquidation result #[derive(Debug)] -pub struct LiquidateObligationResult { +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, @@ -412,8 +412,7 @@ impl ReserveLiquidity { } /// Subtract borrow amount from available liquidity and add to borrows - pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { - let receive_amount = borrow_amount.try_floor_u64()?; + pub fn borrow(&mut self, borrow_amount: Decimal, receive_amount: u64) -> ProgramResult { if receive_amount > self.available_amount { msg!("Borrow amount cannot exceed available amount"); return Err(LendingError::InsufficientLiquidity.into()); @@ -687,8 +686,6 @@ impl IsInitialized for Reserve { } } -// @TODO: Adjust padding, but what's a reasonable number? -// Or should there be no padding to save space, but we need account resizing implemented? const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -797,6 +794,7 @@ impl Pack for Reserve { }) } + // @TODO: break this up by reserve / liquidity / collateral / config fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, RESERVE_LEN]; #[allow(clippy::ptr_offset_with_cast)] diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index 0fac1e06aba..ae6c49b0370 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -17,6 +17,7 @@ use spl_token_lending::{ processor::process_instruction, state::{FeeCalculation, INITIAL_COLLATERAL_RATIO}, }; +use std::u64; #[tokio::test] async fn test_borrow_quote_currency() { @@ -242,7 +243,7 @@ async fn test_borrow_max_base_currency() { ), borrow_obligation_liquidity( spl_token_lending::id(), - u64::max_value(), + u64::MAX, sol_test_reserve.liquidity_supply_pubkey, sol_test_reserve.user_liquidity_pubkey, sol_test_reserve.pubkey, diff --git a/token-lending/program/tests/genesis_accounts.rs b/token-lending/program/tests/genesis_accounts.rs index 83d35ffd930..9672fe9c40e 100644 --- a/token-lending/program/tests/genesis_accounts.rs +++ b/token-lending/program/tests/genesis_accounts.rs @@ -182,7 +182,7 @@ // &payer, // BorrowArgs { // borrow_reserve: &usdc_test_reserve, -// // @FIXME: handle u64::max_value() +// // @FIXME: handle u64::MAX // liquidity_amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, // user_accounts_owner: &user_accounts_owner, // obligation: &usdc_obligation, @@ -197,7 +197,7 @@ // &payer, // BorrowArgs { // borrow_reserve: &usdc_test_reserve, -// // @FIXME: handle u64::max_value() +// // @FIXME: handle u64::MAX // liquidity_amount: lamports_to_usdc_fractional( // usdc_test_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS // / 100, diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index c9f6f8da7cf..ad47fe753dc 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -430,7 +430,7 @@ pub fn add_reserve( config, }); reserve.deposit_liquidity(liquidity_amount).unwrap(); - reserve.liquidity.borrow(borrow_amount.into()).unwrap(); + reserve.liquidity.borrow(borrow_amount.into(), borrow_amount).unwrap(); let borrow_rate_multiplier = Rate::one() .try_add(Rate::from_percent(initial_borrow_rate)) .unwrap(); diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index eebe0e8f711..3863bb07102 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -1,5 +1,8 @@ #![cfg(feature = "test-bpf")] +mod helpers; + +use helpers::*; use solana_program_test::*; use solana_sdk::{ instruction::InstructionError, @@ -7,8 +10,6 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, }; - -use helpers::*; use spl_token::instruction::approve; use spl_token_lending::{ error::LendingError, @@ -16,8 +17,7 @@ use spl_token_lending::{ processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, }; - -mod helpers; +use std::u64; #[tokio::test] async fn test_withdraw_base_currency_fixed_amount() { @@ -190,7 +190,7 @@ async fn test_withdraw_quote_currency_all() { const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; - const WITHDRAW_AMOUNT: u64 = u64::max_value(); + const WITHDRAW_AMOUNT: u64 = u64::MAX; let user_accounts_owner = Keypair::new(); let user_transfer_authority = Keypair::new(); From 37be9f74179c146459137c624cf4211ec1aa4c4a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 13 Apr 2021 19:22:21 -0500 Subject: [PATCH 158/191] update comments --- token-lending/program/src/instruction.rs | 22 ++++++++++++---------- token-lending/program/src/processor.rs | 5 +++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index c3137a1d781..67d48a7c4ae 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -21,10 +21,10 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Lending market account - uninitialized - /// 1. `[]` Quote currency SPL Token mint - /// 2. `[]` Rent sysvar - /// 3. `[]` Token program id + /// 0. `[writable]` Lending market account - uninitialized. + /// 1. `[]` Quote currency SPL Token mint. + /// 2. `[]` Rent sysvar. + /// 3. `[]` Token program id. InitLendingMarket { /// Owner authority which can add new reserves owner: Pubkey, @@ -45,10 +45,11 @@ pub enum LendingInstruction { /// 6. `[writable]` Reserve collateral SPL Token mint - uninitialized. /// 7. `[writable]` Reserve collateral token supply - uninitialized. /// 8. `[]` Lending market account. - /// 9. `[signer]` Lending market owner. /// 10 `[]` Derived lending market authority. /// 11 `[]` User transfer authority ($authority). - /// 12 `[]` Clock sysvar. + // @FIXME: lending market owner is only required because of the trusted aggregator + /// 12 `[signer]` Lending market owner. + /// 13 `[]` Clock sysvar. /// 13 `[]` Rent sysvar. /// 14 `[]` Token program id. /// 15 `[optional]` Reserve liquidity aggregator account. @@ -110,7 +111,8 @@ pub enum LendingInstruction { /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. /// 7. `[signer]` User transfer authority ($authority). - /// 8. `[]` Token program id. + /// 8. `[]` Clock sysvar. + /// 9. `[]` Token program id. RedeemReserveCollateral { /// Amount of collateral tokens to redeem in exchange for liquidity collateral_amount: u64, @@ -190,7 +192,7 @@ pub enum LendingInstruction { }, // 3 - /// Accrue interest and update median quote token price on a reserve. + /// Accrue interest and update market price of liquidity on a reserve. /// /// Accounts expected by this instruction: /// @@ -254,8 +256,8 @@ pub enum LendingInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Lending market account - /// 1. `[signer]` Current owner + /// 0. `[writable]` Lending market account. + /// 1. `[signer]` Current owner. SetLendingMarketOwner { /// The new owner new_owner: Pubkey, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index ab81015c4e1..600ed4bd691 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -187,6 +187,7 @@ fn process_init_reserve( let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_collateral_supply_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + // @FIXME: lending market owner is only required because of the trusted aggregator let lending_market_owner_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; @@ -203,6 +204,7 @@ fn process_init_reserve( } assert_uninitialized::(reserve_liquidity_supply_info)?; + // @TODO: why does the liquidity fee receiver need to be uninitialized? assert_uninitialized::(reserve_liquidity_fee_receiver_info)?; assert_uninitialized::(reserve_collateral_mint_info)?; assert_uninitialized::(reserve_collateral_supply_info)?; @@ -222,6 +224,7 @@ fn process_init_reserve( msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } + // @FIXME: lending market owner is only required because of the trusted aggregator if &lending_market.owner != lending_market_owner_info.key { msg!("Lending market owner does not match the lending market owner provided"); return Err(LendingError::InvalidMarketOwner.into()); @@ -231,6 +234,8 @@ fn process_init_reserve( return Err(LendingError::InvalidSigner.into()); } + // @FIXME: only the lending market owner can create a reserve because there is no way to + // verify that the aggregator represents the price of any particular token let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { if account_info_iter.peek().is_some() { From e3dd78111dceded145cd04a8a79c8d3f6c59ef60 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 14 Apr 2021 14:59:04 -0500 Subject: [PATCH 159/191] review fixes --- token-lending/program/src/processor.rs | 24 ++++---- token-lending/program/src/state/obligation.rs | 60 +++++++++++-------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 600ed4bd691..f00da385f76 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -272,7 +272,6 @@ fn process_init_reserve( return Err(LendingError::InvalidTokenOwner.into()); } - reserve.version = PROGRAM_VERSION; reserve.init(InitReserveParams { current_slot: clock.slot, lending_market: *lending_market_info.key, @@ -383,8 +382,8 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro current_slot: clock.slot, lending_market: *lending_market_info.key, owner: *obligation_owner_info.key, - deposits: Vec::with_capacity(0), - borrows: Vec::with_capacity(0), + deposits: vec![], + borrows: vec![], }); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -695,9 +694,9 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let max_borrow_value = obligation.max_borrow_value()?; - if max_borrow_value == Decimal::zero() { - msg!("Maximum borrow value is zero"); + let remaining_borrow_value = obligation.max_borrow_value()?; + if remaining_borrow_value == Decimal::zero() { + msg!("Remaining borrow value is zero"); return Err(LendingError::BorrowTooLarge.into()); } @@ -706,7 +705,7 @@ fn process_borrow_obligation_liquidity( receive_amount, borrow_fee, host_fee, - } = borrow_reserve.calculate_borrow(liquidity_amount, max_borrow_value)?; + } = borrow_reserve.calculate_borrow(liquidity_amount, remaining_borrow_value)?; if receive_amount == 0 { msg!("Borrow amount is too small to receive liquidity after fees"); @@ -1374,11 +1373,12 @@ fn process_withdraw_obligation_collateral( let withdraw_amount = if obligation.borrows.is_empty() { // there are no borrows; they have been repaid, liquidated, or were never taken out - collateral.deposited_amount - } else if obligation.borrowed_value == Decimal::zero() { - // there are borrows, but they cannot be valued; they must be repaid to withdraw collateral - msg!("Obligation borrowed value is zero"); - return Err(LendingError::ObligationBorrowsZero.into()); + if collateral_amount == u64::MAX { + collateral.deposited_amount + } + else { + collateral.deposited_amount.min(collateral_amount) + } } else if obligation.deposited_value == Decimal::zero() { // there are deposits, but they cannot be valued msg!("Obligation deposited value is zero"); diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 8cf7ed5db44..66664daa0c5 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -12,7 +12,10 @@ use solana_program::{ program_pack::{IsInitialized, Pack, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; -use std::convert::{TryFrom, TryInto}; +use std::{ + cmp::Ordering, + convert::{TryFrom, TryInto} +}; /// Max number of collateral and liquidity reserve accounts combined for an obligation pub const MAX_OBLIGATION_RESERVES: usize = 10; @@ -60,19 +63,6 @@ impl Obligation { self.borrows = params.borrows; } - /// 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); - let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; - liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct) - } - /// 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) @@ -118,6 +108,19 @@ impl Obligation { max_borrowed_value.try_sub(self.borrowed_value) } + /// 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); + 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, @@ -320,20 +323,25 @@ impl ObligationLiquidity { /// Accrue interest pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { - if cumulative_borrow_rate_wads < self.cumulative_borrow_rate_wads { - msg!("Interest rate cannot be negative"); - return Err(LendingError::NegativeInterestRate.into()); + 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; + } } - 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(()) } } From 9f2ea9ae4985277e8c44c3571eb920d7e945de4c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 14 Apr 2021 15:00:17 -0500 Subject: [PATCH 160/191] mint_supply / mint_total_supply -> mint_amount supply is used elsewhere to represent pubkeys --- token-lending/program/src/state/reserve.rs | 26 +++++++++++----------- token-lending/program/tests/helpers/mod.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 24ad8f1bee9..6cb74c2368f 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -489,7 +489,7 @@ pub struct ReserveCollateral { /// Reserve collateral mint address pub mint_pubkey: Pubkey, /// Reserve collateral mint supply, used for exchange rate - pub mint_total_supply: u64, + pub mint_amount: u64, /// Reserve collateral supply address pub supply_pubkey: Pubkey, } @@ -499,15 +499,15 @@ impl ReserveCollateral { pub fn new(params: NewReserveCollateralParams) -> Self { Self { mint_pubkey: params.mint_pubkey, - mint_total_supply: 0, + mint_amount: 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 + self.mint_amount = self + .mint_amount .checked_add(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -515,8 +515,8 @@ impl ReserveCollateral { /// Remove collateral from total supply pub fn burn(&mut self, collateral_amount: u64) -> ProgramResult { - self.mint_total_supply = self - .mint_total_supply + self.mint_amount = self + .mint_amount .checked_sub(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -527,11 +527,11 @@ impl ReserveCollateral { &self, total_liquidity: Decimal, ) -> Result { - let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() { + let rate = if self.mint_amount == 0 || total_liquidity == Decimal::zero() { Rate::from_scaled_val(INITIAL_COLLATERAL_RATE) } else { - let collateral_supply = Decimal::from(self.mint_total_supply); - Rate::try_from(collateral_supply.try_div(total_liquidity)?)? + let mint_amount = Decimal::from(self.mint_amount); + Rate::try_from(mint_amount.try_div(total_liquidity)?)? }; Ok(CollateralExchangeRate(rate)) @@ -718,7 +718,7 @@ impl Pack for Reserve { liquidity_cumulative_borrow_rate_wads, liquidity_borrowed_amount_wads, liquidity_available_amount, - collateral_mint_supply, + collateral_mint_amount, liquidity_market_price, _padding, ) = array_refs![ @@ -775,7 +775,7 @@ impl Pack for Reserve { }, collateral: ReserveCollateral { mint_pubkey: Pubkey::new_from_array(*collateral_mint), - mint_total_supply: u64::from_le_bytes(*collateral_mint_supply), + mint_amount: u64::from_le_bytes(*collateral_mint_amount), supply_pubkey: Pubkey::new_from_array(*collateral_supply), }, config: ReserveConfig { @@ -822,7 +822,7 @@ impl Pack for Reserve { liquidity_cumulative_borrow_rate_wads, liquidity_borrowed_amount_wads, liquidity_available_amount, - collateral_mint_supply, + collateral_mint_amount, liquidity_market_price, _padding, ) = mut_array_refs![ @@ -877,7 +877,7 @@ impl Pack for Reserve { // collateral info collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - *collateral_mint_supply = self.collateral.mint_total_supply.to_le_bytes(); + *collateral_mint_amount = self.collateral.mint_amount.to_le_bytes(); collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index ad47fe753dc..2ecbd828085 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -1032,7 +1032,7 @@ impl TestReserve { ); assert_eq!(reserve.liquidity.borrowed_amount_wads, Decimal::zero()); assert!(reserve.liquidity.available_amount > 0); - assert!(reserve.collateral.mint_total_supply > 0); + assert!(reserve.collateral.mint_amount > 0); } } From 81a1002be0011d4acc8b22a5eb14428d114bda3c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 14 Apr 2021 16:48:32 -0500 Subject: [PATCH 161/191] remove obligation tokens --- token-lending/program/src/instruction.rs | 43 ++---- token-lending/program/src/processor.rs | 127 ++---------------- token-lending/program/src/state/obligation.rs | 38 ++---- .../tests/deposit_obligation_collateral.rs | 25 ---- token-lending/program/tests/helpers/mod.rs | 35 +---- .../program/tests/obligation_end_to_end.rs | 63 ++------- .../tests/withdraw_obligation_collateral.rs | 53 -------- 7 files changed, 48 insertions(+), 336 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 67d48a7c4ae..400d7d45f64 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -137,7 +137,7 @@ pub enum LendingInstruction { /// 7. `[signer]` Obligation owner. /// 8. `[]` Clock sysvar. /// 9. `[]` Token program id. - /// 10. `[optional, writable]` Host fee receiver account. + /// 10 `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power liquidity_amount: u64, @@ -214,15 +214,12 @@ pub enum LendingInstruction { /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. /// 2. `[]` Deposit reserve account - refreshed. /// 3. `[writable]` Obligation account. - /// 4. `[writable]` Obligation token mint. - /// 5. `[writable]` Obligation token output account. - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` Obligation owner. - /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Clock sysvar. - /// 11 `[]` Rent sysvar. - /// 12 `[]` Token program id. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[signer]` Obligation owner. + /// 7. `[signer]` User transfer authority ($authority). + /// 8. `[]` Clock sysvar. + /// 9. `[]` Token program id. DepositObligationCollateral { /// Amount of collateral tokens to deposit collateral_amount: u64, @@ -236,16 +233,13 @@ pub enum LendingInstruction { /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. /// 1. `[writable]` Destination collateral token account. /// Minted by withdraw reserve collateral mint. - /// $authority can transfer $collateral_amount. /// 2. `[]` Withdraw reserve account - refreshed. /// 3. `[writable]` Obligation account - refreshed. - /// 4. `[writable]` Obligation token mint. - /// 5. `[writable]` Obligation token input account. - /// 6. `[]` Lending market account. - /// 7. `[]` Derived lending market authority. - /// 8. `[signer]` User transfer authority ($authority). - /// 9. `[]` Clock sysvar. - /// 10 `[]` Token program id. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[signer]` Obligation owner. + /// 7. `[]` Clock sysvar. + /// 8. `[]` Token program id. WithdrawObligationCollateral { /// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount collateral_amount: u64, @@ -790,8 +784,6 @@ pub fn deposit_obligation_collateral( destination_collateral_pubkey: Pubkey, deposit_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_token_mint_pubkey: Pubkey, - obligation_token_output_pubkey: Pubkey, lending_market_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, @@ -807,14 +799,11 @@ pub fn deposit_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(deposit_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_token_mint_pubkey, false), - AccountMeta::new(obligation_token_output_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), @@ -830,10 +819,8 @@ pub fn withdraw_obligation_collateral( destination_collateral_pubkey: Pubkey, withdraw_reserve_pubkey: Pubkey, obligation_pubkey: Pubkey, - obligation_mint_pubkey: Pubkey, - obligation_input_pubkey: Pubkey, lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, + obligation_owner_pubkey: Pubkey, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], @@ -846,11 +833,9 @@ pub fn withdraw_obligation_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new_readonly(withdraw_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(obligation_mint_pubkey, false), - AccountMeta::new(obligation_input_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f00da385f76..6917efe445d 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -660,10 +660,6 @@ fn process_borrow_obligation_liquidity( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.last_update.is_stale(clock.slot)? { - msg!("Obligation is stale and must be refreshed in the current slot"); - return Err(LendingError::ObligationStale.into()); - } if &obligation.owner != obligation_owner_info.key { msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); @@ -672,6 +668,10 @@ fn process_borrow_obligation_liquidity( msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); + return Err(LendingError::ObligationStale.into()); + } if obligation.deposits.is_empty() { msg!("Obligation has no deposits to borrow against"); return Err(LendingError::ObligationDepositsEmpty.into()); @@ -1098,14 +1098,11 @@ fn process_deposit_obligation_collateral( let destination_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_output_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent_info = next_account_info(account_info_iter)?; let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -1164,54 +1161,6 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidSigner.into()); } - // @TODO: Does there need to be a check to make sure obligation_token_mint_info is rent exempt? - let obligation_token_mint = Mint::unpack_unchecked(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint.is_initialized() { - if obligation_token_mint_info.owner != token_program_id.key { - msg!("Obligation token mint provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) - { - msg!("Obligation token mint authority does not match the lending market authority provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - } else { - spl_token_init_mint(TokenInitializeMintParams { - mint: obligation_token_mint_info.clone(), - authority: lending_market_authority_info.key, - rent: rent_info.clone(), - decimals: deposit_reserve.liquidity.mint_decimals, - token_program: token_program_id.clone(), - })?; - } - - // @TODO: Does there need to be a check to make sure obligation_token_output_info is rent exempt? - let obligation_token_output = - Account::unpack_unchecked(&obligation_token_output_info.data.borrow())?; - if obligation_token_output.is_initialized() { - if obligation_token_output_info.owner != token_program_id.key { - msg!("Obligation token output provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation_token_output.mint != obligation_token_mint_info.key { - msg!("Obligation token output mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } - if &obligation_token_output.owner != obligation_owner_info.key { - msg!("Obligation token output owner does not match the obligation owner provided"); - return Err(LendingError::InvalidObligationOwner.into()); - } - } else { - spl_token_init_account(TokenInitializeAccountParams { - account: obligation_token_output_info.clone(), - mint: obligation_token_mint_info.clone(), - owner: obligation_owner_info.clone(), - rent: rent_info.clone(), - token_program: token_program_id.clone(), - })?; - } - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -1226,10 +1175,7 @@ fn process_deposit_obligation_collateral( } obligation - .find_or_add_collateral_to_deposits( - *deposit_reserve_info.key, - *obligation_token_mint_info.key, - )? + .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1243,15 +1189,6 @@ fn process_deposit_obligation_collateral( token_program: token_program_id.clone(), })?; - spl_token_mint_to(TokenMintToParams { - mint: obligation_token_mint_info.clone(), - destination: obligation_token_output_info.clone(), - amount: collateral_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; - Ok(()) } @@ -1271,11 +1208,9 @@ fn process_withdraw_obligation_collateral( let destination_collateral_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; - let obligation_token_mint_info = next_account_info(account_info_iter)?; - let obligation_token_input_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1320,6 +1255,14 @@ fn process_withdraw_obligation_collateral( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } + if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); + return Err(LendingError::InvalidObligationOwner.into()); + } + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); @@ -1327,37 +1270,11 @@ fn process_withdraw_obligation_collateral( let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - if &collateral.token_mint != obligation_token_mint_info.key { - msg!("Collateral token mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } if collateral.deposited_amount == 0 { msg!("Collateral deposited amount is zero"); return Err(LendingError::ObligationCollateralEmpty.into()); } - let obligation_token_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?; - if obligation_token_mint_info.owner != token_program_id.key { - msg!("Obligation token mint provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if obligation_token_mint.mint_authority != COption::Some(*lending_market_authority_info.key) { - msg!( - "Obligation token mint authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidAccountOwner.into()); - } - - let obligation_token_input = Account::unpack(&obligation_token_input_info.data.borrow())?; - if obligation_token_input_info.owner != token_program_id.key { - msg!("Obligation token input provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if &obligation_token_input.mint != obligation_token_mint_info.key { - msg!("Obligation token input mint does not match the obligation token mint provided"); - return Err(LendingError::InvalidTokenMint.into()); - } - let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -1416,26 +1333,10 @@ fn process_withdraw_obligation_collateral( withdraw_amount }; - let obligation_token_amount = collateral - .collateral_to_obligation_token_amount(withdraw_amount, obligation_token_mint.supply)?; - if obligation_token_amount == 0 { - msg!("Withdraw amount is too small to burn obligation tokens"); - return Err(LendingError::WithdrawTooSmall.into()); - } - obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - spl_token_burn(TokenBurnParams { - mint: obligation_token_mint_info.clone(), - source: obligation_token_input_info.clone(), - amount: obligation_token_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; - spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 66664daa0c5..a300e4aaea7 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -140,13 +140,8 @@ impl Obligation { pub fn find_or_add_collateral_to_deposits( &mut self, deposit_reserve: Pubkey, - token_mint: Pubkey, ) -> Result<&mut ObligationCollateral, ProgramError> { if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) { - if self.deposits[collateral_index].token_mint != token_mint { - msg!("Token mint of obligation collateral {} does not match the obligation token mint provided", collateral_index); - return Err(LendingError::InvalidTokenMint.into()); - } return Ok(&mut self.deposits[collateral_index]); } if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { @@ -156,7 +151,7 @@ impl Obligation { ); return Err(LendingError::ObligationReserveLimit.into()); } - let collateral = ObligationCollateral::new(deposit_reserve, token_mint); + let collateral = ObligationCollateral::new(deposit_reserve); self.deposits.push(collateral); Ok(self.deposits.last_mut().unwrap()) } @@ -235,8 +230,6 @@ impl IsInitialized for Obligation { pub struct ObligationCollateral { /// Reserve collateral is deposited to pub deposit_reserve: Pubkey, - /// Mint address of the tokens for this obligation collateral - pub token_mint: Pubkey, /// Amount of collateral deposited pub deposited_amount: u64, /// Collateral market value in quote currency @@ -245,10 +238,9 @@ pub struct ObligationCollateral { impl ObligationCollateral { /// Create new obligation collateral - pub fn new(deposit_reserve: Pubkey, token_mint: Pubkey) -> Self { + pub fn new(deposit_reserve: Pubkey) -> Self { Self { deposit_reserve, - token_mint, deposited_amount: 0, market_value: Decimal::zero(), } @@ -271,18 +263,6 @@ impl ObligationCollateral { .ok_or(LendingError::MathOverflow)?; Ok(()) } - - /// Amount of obligation tokens for given collateral - pub fn collateral_to_obligation_token_amount( - &self, - collateral_amount: u64, - obligation_token_supply: u64, - ) -> Result { - let withdraw_pct = Decimal::from(collateral_amount).try_div(self.deposited_amount)?; - withdraw_pct - .try_mul(obligation_token_supply)? - .try_floor_u64() - } } /// Obligation liquidity state @@ -348,9 +328,9 @@ impl ObligationLiquidity { // @TODO: Adjust padding, but what's a reasonable number? // Or should there be no padding to save space, but we need account resizing implemented? -const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 32 + 8 + 16 +const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 -const OBLIGATION_LEN: usize = 948; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (88 * 1) + (80 * 9) +const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9) // @TODO: break this up by obligation / collateral / liquidity impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; @@ -401,11 +381,10 @@ impl Pack for Obligation { 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, token_mint, deposited_amount, market_value) = - array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; + let (deposit_reserve, deposited_amount, market_value) = + array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; deposits.push(ObligationCollateral { deposit_reserve: Pubkey::new(deposit_reserve), - token_mint: Pubkey::new(token_mint), deposited_amount: u64::from_le_bytes(*deposited_amount), market_value: unpack_decimal(market_value), }); @@ -490,10 +469,9 @@ impl Pack for Obligation { 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, token_mint, deposited_amount, market_value) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, PUBKEY_BYTES, 8, 16]; + let (deposit_reserve, deposited_amount, market_value) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); - token_mint.copy_from_slice(collateral.token_mint.as_ref()); *deposited_amount = collateral.deposited_amount.to_le_bytes(); pack_decimal(collateral.market_value, market_value); offset += OBLIGATION_COLLATERAL_LEN; diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index ed15c7473bd..3752021f136 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -36,8 +36,6 @@ async fn test_success() { let user_accounts_owner = Keypair::new(); let user_transfer_authority = Keypair::new(); - let obligation_token_mint_keypair = Keypair::new(); - let obligation_token_account_keypair = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -77,20 +75,6 @@ async fn test_success() { let mut transaction = Transaction::new_with_payer( &[ - create_account( - &payer.pubkey(), - &obligation_token_mint_keypair.pubkey(), - rent.minimum_balance(Mint::LEN), - Mint::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &obligation_token_account_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), approve( &spl_token::id(), &sol_test_reserve.user_collateral_pubkey, @@ -107,8 +91,6 @@ async fn test_success() { sol_test_reserve.collateral_supply_pubkey, sol_test_reserve.pubkey, test_obligation.pubkey, - obligation_token_mint_keypair.pubkey(), - obligation_token_account_keypair.pubkey(), lending_market.pubkey, test_obligation.owner, user_transfer_authority.pubkey(), @@ -120,8 +102,6 @@ async fn test_success() { transaction.sign( &vec![ &payer, - &obligation_token_mint_keypair, - &obligation_token_account_keypair, &user_accounts_owner, &user_transfer_authority, ], @@ -142,9 +122,4 @@ async fn test_success() { user_collateral_balance, initial_user_collateral_balance - SOL_DEPOSIT_AMOUNT_LAMPORTS ); - - // check that obligation tokens were minted - let obligation_token_balance = - get_token_balance(&mut banks_client, obligation_token_account_keypair.pubkey()).await; - assert_eq!(obligation_token_balance, SOL_DEPOSIT_AMOUNT_LAMPORTS); } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 2ecbd828085..1eb7ab0ee9d 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -178,36 +178,8 @@ pub fn add_obligation( let (obligation_deposits, test_deposits) = deposits .iter() .map(|(deposit_reserve, collateral_amount)| { - let token_mint_pubkey = Pubkey::new_unique(); - test.add_packable_account( - token_mint_pubkey, - u32::MAX as u64, - &Mint { - is_initialized: true, - decimals: deposit_reserve.liquidity_mint_decimals, - mint_authority: COption::Some(lending_market.authority), - supply: *collateral_amount, - ..Mint::default() - }, - &spl_token::id(), - ); - - let token_account_pubkey = Pubkey::new_unique(); - test.add_packable_account( - token_account_pubkey, - u32::MAX as u64, - &Token { - mint: token_mint_pubkey, - owner: user_accounts_owner.pubkey(), - state: AccountState::Initialized, - amount: *collateral_amount, - ..Token::default() - }, - &spl_token::id(), - ); - let mut collateral = - ObligationCollateral::new(deposit_reserve.pubkey, token_mint_pubkey); + ObligationCollateral::new(deposit_reserve.pubkey); collateral.deposited_amount = *collateral_amount; ( @@ -215,8 +187,6 @@ pub fn add_obligation( TestObligationCollateral { obligation_pubkey, deposit_reserve: deposit_reserve.pubkey, - token_mint: token_mint_pubkey, - token_account: token_account_pubkey, deposited_amount: *collateral_amount, }, ) @@ -1117,8 +1087,6 @@ impl TestObligation { pub struct TestObligationCollateral { pub obligation_pubkey: Pubkey, pub deposit_reserve: Pubkey, - pub token_mint: Pubkey, - pub token_account: Pubkey, pub deposited_amount: u64, } @@ -1139,7 +1107,6 @@ impl TestObligationCollateral { let (collateral, _) = obligation .find_collateral_in_deposits(self.deposit_reserve) .unwrap(); - assert_eq!(collateral.token_mint, self.token_mint); assert_eq!(collateral.deposited_amount, self.deposited_amount); } } diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index 8ccd52ae83e..381581d2ca5 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -59,12 +59,6 @@ async fn test_success() { let obligation_keypair = Keypair::new(); let obligation_pubkey = obligation_keypair.pubkey(); - let obligation_token_mint_keypair = Keypair::new(); - let obligation_token_mint_pubkey = obligation_token_mint_keypair.pubkey(); - - let obligation_token_account_keypair = Keypair::new(); - let obligation_token_account_pubkey = obligation_token_account_keypair.pubkey(); - let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -131,28 +125,12 @@ async fn test_success() { user_accounts_owner_pubkey, ), // 2 - create_account( - &payer_pubkey, - &obligation_token_mint_pubkey, - rent.minimum_balance(Mint::LEN), - Mint::LEN as u64, - &spl_token::id(), - ), - // 3 - create_account( - &payer_pubkey, - &obligation_token_account_pubkey, - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - // 4 refresh_reserve( spl_token_lending::id(), sol_test_reserve.pubkey, sol_test_reserve.liquidity_aggregator_pubkey, ), - // 5 + // 3 approve( &spl_token::id(), &sol_test_reserve.user_collateral_pubkey, @@ -162,7 +140,7 @@ async fn test_success() { SOL_DEPOSIT_AMOUNT_LAMPORTS, ) .unwrap(), - // 6 + // 4 deposit_obligation_collateral( spl_token_lending::id(), SOL_DEPOSIT_AMOUNT_LAMPORTS, @@ -170,21 +148,19 @@ async fn test_success() { sol_test_reserve.collateral_supply_pubkey, sol_test_reserve.pubkey, obligation_pubkey, - obligation_token_mint_pubkey, - obligation_token_account_pubkey, lending_market.pubkey, user_accounts_owner_pubkey, user_transfer_authority_pubkey, ), - // 7 + // 5 refresh_obligation( spl_token_lending::id(), obligation_pubkey, vec![sol_test_reserve.pubkey], ), - // 8 + // 6 refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), - // 9 + // 7 borrow_obligation_liquidity( spl_token_lending::id(), USDC_BORROW_AMOUNT_FRACTIONAL, @@ -197,15 +173,15 @@ async fn test_success() { user_accounts_owner_pubkey, Some(usdc_test_reserve.liquidity_host_pubkey), ), - // 10 + // 8 refresh_reserve(spl_token_lending::id(), usdc_test_reserve.pubkey, None), - // 11 + // 9 refresh_obligation( spl_token_lending::id(), obligation_pubkey, vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], ), - // 12 + // 10 approve( &spl_token::id(), &usdc_test_reserve.user_liquidity_pubkey, @@ -215,7 +191,7 @@ async fn test_success() { USDC_REPAY_AMOUNT_FRACTIONAL, ) .unwrap(), - // 13 + // 11 repay_obligation_liquidity( spl_token_lending::id(), USDC_REPAY_AMOUNT_FRACTIONAL, @@ -226,23 +202,13 @@ async fn test_success() { lending_market.pubkey, user_transfer_authority_pubkey, ), - // 14 + // 12 refresh_obligation( spl_token_lending::id(), obligation_pubkey, vec![sol_test_reserve.pubkey], ), - // 15 - approve( - &spl_token::id(), - &obligation_token_account_pubkey, - &user_transfer_authority_pubkey, - &user_accounts_owner_pubkey, - &[], - SOL_DEPOSIT_AMOUNT_LAMPORTS, - ) - .unwrap(), - // 16 + // 13 withdraw_obligation_collateral( spl_token_lending::id(), SOL_DEPOSIT_AMOUNT_LAMPORTS, @@ -250,8 +216,6 @@ async fn test_success() { sol_test_reserve.user_collateral_pubkey, sol_test_reserve.pubkey, obligation_pubkey, - obligation_token_mint_pubkey, - obligation_token_account_pubkey, lending_market.pubkey, user_transfer_authority_pubkey, ), @@ -264,8 +228,6 @@ async fn test_success() { &payer, &obligation_keypair, &user_accounts_owner, - &obligation_token_mint_keypair, - &obligation_token_account_keypair, &user_transfer_authority, ], recent_blockhash, @@ -306,9 +268,6 @@ async fn test_success() { initial_liquidity_supply ); - let obligation_token_balance = - get_token_balance(&mut banks_client, obligation_token_account_pubkey).await; - assert_eq!(obligation_token_balance, 0); assert_eq!(obligation.deposits.len(), 0); assert_eq!(obligation.borrows.len(), 0); diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 3863bb07102..6c7ea63f5ea 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -96,8 +96,6 @@ async fn test_withdraw_base_currency_fixed_amount() { get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; let initial_user_collateral_balance = get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let initial_obligation_token_balance = - get_token_balance(&mut banks_client, test_collateral.token_account).await; let mut transaction = Transaction::new_with_payer( &[ @@ -110,15 +108,6 @@ async fn test_withdraw_base_currency_fixed_amount() { WITHDRAW_AMOUNT, ) .unwrap(), - approve( - &spl_token::id(), - &test_collateral.token_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -131,8 +120,6 @@ async fn test_withdraw_base_currency_fixed_amount() { sol_test_reserve.user_collateral_pubkey, sol_test_reserve.pubkey, test_obligation.pubkey, - test_collateral.token_mint, - test_collateral.token_account, lending_market.pubkey, user_transfer_authority.pubkey(), ), @@ -160,14 +147,6 @@ async fn test_withdraw_base_currency_fixed_amount() { initial_user_collateral_balance + WITHDRAW_AMOUNT ); - // check that obligation tokens were burned - let obligation_token_balance = - get_token_balance(&mut banks_client, test_collateral.token_account).await; - assert_eq!( - obligation_token_balance, - initial_obligation_token_balance - WITHDRAW_AMOUNT - ); - let obligation = test_obligation.get_state(&mut banks_client).await; let collateral = &obligation.deposits[0]; assert_eq!( @@ -238,8 +217,6 @@ async fn test_withdraw_quote_currency_all() { .await; let initial_user_collateral_balance = get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; - let initial_obligation_token_balance = - get_token_balance(&mut banks_client, test_collateral.token_account).await; let mut transaction = Transaction::new_with_payer( &[ @@ -252,15 +229,6 @@ async fn test_withdraw_quote_currency_all() { WITHDRAW_AMOUNT, ) .unwrap(), - approve( - &spl_token::id(), - &test_collateral.token_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -273,8 +241,6 @@ async fn test_withdraw_quote_currency_all() { usdc_test_reserve.user_collateral_pubkey, usdc_test_reserve.pubkey, test_obligation.pubkey, - test_collateral.token_mint, - test_collateral.token_account, lending_market.pubkey, user_transfer_authority.pubkey(), ), @@ -305,14 +271,6 @@ async fn test_withdraw_quote_currency_all() { initial_user_collateral_balance + USDC_DEPOSIT_AMOUNT_FRACTIONAL ); - // check that obligation tokens were burned - let obligation_token_balance = - get_token_balance(&mut banks_client, test_collateral.token_account).await; - assert_eq!( - obligation_token_balance, - initial_obligation_token_balance - USDC_DEPOSIT_AMOUNT_FRACTIONAL - ); - let obligation = test_obligation.get_state(&mut banks_client).await; assert_eq!(obligation.deposits.len(), 0); } @@ -393,15 +351,6 @@ async fn test_withdraw_too_large() { WITHDRAW_AMOUNT, ) .unwrap(), - approve( - &spl_token::id(), - &test_collateral.token_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -414,8 +363,6 @@ async fn test_withdraw_too_large() { sol_test_reserve.user_collateral_pubkey, sol_test_reserve.pubkey, test_obligation.pubkey, - test_collateral.token_mint, - test_collateral.token_account, lending_market.pubkey, user_transfer_authority.pubkey(), ), From cb42fd0eec7db1d1ff36689fa31b026490eaa4a2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Wed, 14 Apr 2021 16:50:00 -0500 Subject: [PATCH 162/191] cargo fmt/clippy --- token-lending/program/src/math/rate.rs | 6 +----- token-lending/program/src/processor.rs | 20 +++++++++---------- token-lending/program/src/state/obligation.rs | 5 ++--- token-lending/program/src/state/reserve.rs | 2 +- .../tests/deposit_obligation_collateral.rs | 6 +----- token-lending/program/tests/helpers/mod.rs | 8 +++++--- 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 0661e494efa..5f6d2fed2c1 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -23,11 +23,7 @@ use crate::{ math::{common::*, decimal::Decimal}, }; use solana_program::program_error::ProgramError; -use std::{ - convert::TryFrom, - fmt, - u64 -}; +use std::{convert::TryFrom, fmt, u64}; use uint::construct_uint; // U128 with 128 bits consisting of 2 x 64-bit words diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6917efe445d..c34b440eed5 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -5,10 +5,10 @@ use crate::{ instruction::LendingInstruction, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, WAD}, state::{ - CalculateBorrowResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, - LendingMarket, CalculateLiquidationResult, NewReserveCollateralParams, - NewReserveLiquidityParams, Obligation, CalculateRepayResult, Reserve, ReserveCollateral, - ReserveConfig, ReserveLiquidity, PROGRAM_VERSION, + CalculateBorrowResult, CalculateLiquidationResult, CalculateRepayResult, + InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, + NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, Reserve, + ReserveCollateral, ReserveConfig, ReserveLiquidity, }, }; use flux_aggregator::read_median; @@ -26,10 +26,7 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::{Account, Mint}; -use std::{ - convert::TryFrom, - u64 -}; +use std::{convert::TryFrom, u64}; /// Processes an instruction pub fn process_instruction( @@ -712,7 +709,9 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::BorrowTooSmall.into()); } - borrow_reserve.liquidity.borrow(borrow_amount, receive_amount)?; + borrow_reserve + .liquidity + .borrow(borrow_amount, receive_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; @@ -1292,8 +1291,7 @@ fn process_withdraw_obligation_collateral( // there are no borrows; they have been repaid, liquidated, or were never taken out if collateral_amount == u64::MAX { collateral.deposited_amount - } - else { + } else { collateral.deposited_amount.min(collateral_amount) } } else if obligation.deposited_value == Decimal::zero() { diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index a300e4aaea7..3959f87c96c 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -14,7 +14,7 @@ use solana_program::{ }; use std::{ cmp::Ordering, - convert::{TryFrom, TryInto} + convert::{TryFrom, TryInto}, }; /// Max number of collateral and liquidity reserve accounts combined for an obligation @@ -308,8 +308,7 @@ impl ObligationLiquidity { msg!("Interest rate cannot be negative"); return Err(LendingError::NegativeInterestRate.into()); } - Ordering::Equal => { - } + Ordering::Equal => {} Ordering::Greater => { let compounded_interest_rate: Rate = cumulative_borrow_rate_wads .try_div(self.cumulative_borrow_rate_wads)? diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 6cb74c2368f..e0554ffa4c1 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -16,7 +16,7 @@ use solana_program::{ use std::{ cmp::Ordering, convert::{TryFrom, TryInto}, - u64 + u64, }; /// Percentage of an obligation that can be repaid during each liquidation call diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index 3752021f136..338c4546773 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -100,11 +100,7 @@ async fn test_success() { ); transaction.sign( - &vec![ - &payer, - &user_accounts_owner, - &user_transfer_authority, - ], + &vec![&payer, &user_accounts_owner, &user_transfer_authority], recent_blockhash, ); assert!(banks_client.process_transaction(transaction).await.is_ok()); diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 1eb7ab0ee9d..32efdd5b1a7 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -178,8 +178,7 @@ pub fn add_obligation( let (obligation_deposits, test_deposits) = deposits .iter() .map(|(deposit_reserve, collateral_amount)| { - let mut collateral = - ObligationCollateral::new(deposit_reserve.pubkey); + let mut collateral = ObligationCollateral::new(deposit_reserve.pubkey); collateral.deposited_amount = *collateral_amount; ( @@ -400,7 +399,10 @@ pub fn add_reserve( config, }); reserve.deposit_liquidity(liquidity_amount).unwrap(); - reserve.liquidity.borrow(borrow_amount.into(), borrow_amount).unwrap(); + reserve + .liquidity + .borrow(borrow_amount.into(), borrow_amount) + .unwrap(); let borrow_rate_multiplier = Rate::one() .try_add(Rate::from_percent(initial_borrow_rate)) .unwrap(); From 0e7e100772cdf85f20632117a531c0099be26264 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 14:08:46 -0500 Subject: [PATCH 163/191] add comments --- token-lending/program/src/state/reserve.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e0554ffa4c1..34151d19449 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -644,7 +644,9 @@ impl ReserveFees { }; let borrow_fee_amount = match fee_calculation { + // Calculate fee to be added to borrow: fee = amount * rate FeeCalculation::Exclusive => borrow_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())?)?; From c2f2cbe03098f80bb7f443b0a949bad759beb6c9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 14:11:47 -0500 Subject: [PATCH 164/191] misc todos / test fixes --- token-lending/program/src/processor.rs | 1 + token-lending/program/tests/init_reserve.rs | 2 +- token-lending/program/tests/obligation_end_to_end.rs | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c34b440eed5..dc06e45a496 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -243,6 +243,7 @@ fn process_init_reserve( (COption::None, 1) } else { let aggregator_info = next_account_info(account_info_iter)?; + // @FIXME: check aggregator decimals to match lending_market.quote_token_mint decimals assert_rent_exempt(rent, aggregator_info)?; ( COption::Some(*aggregator_info.key), diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index df6ca6e07ee..7bba2ab110d 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -26,7 +26,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(61_000); + test.set_bpf_compute_max_units(62_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index 381581d2ca5..a7963a50334 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -217,7 +217,7 @@ async fn test_success() { sol_test_reserve.pubkey, obligation_pubkey, lending_market.pubkey, - user_transfer_authority_pubkey, + user_accounts_owner_pubkey, ), ], Some(&payer_pubkey), @@ -263,6 +263,10 @@ async fn test_success() { initial_user_liquidity_balance - FEE_AMOUNT ); assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, Decimal::zero()); + // @FIXME: + // thread 'test_success' panicked at 'assertion failed: `(left == right)` + // left: `2000000000`, + // right: `1999999900`', token-lending/program/tests/obligation_end_to_end.rs:266:5 assert_eq!( usdc_reserve.liquidity.available_amount, initial_liquidity_supply From 0fa9002c7f017cbf5f87547e4324394c98d7f123 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 14:20:43 -0500 Subject: [PATCH 165/191] reordering instructions and processors --- token-lending/program/src/instruction.rs | 532 +++++----- token-lending/program/src/processor.rs | 1156 +++++++++++----------- 2 files changed, 844 insertions(+), 844 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 400d7d45f64..48d305d3cb9 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -30,6 +30,18 @@ pub enum LendingInstruction { owner: Pubkey, }, + // 1 + /// Sets the new owner of a lending market. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Lending market account. + /// 1. `[signer]` Current owner. + SetLendingMarketOwner { + /// The new owner + new_owner: Pubkey, + }, + // 2 /// Initializes a new lending market reserve. /// @@ -62,18 +74,17 @@ pub enum LendingInstruction { config: ReserveConfig, }, - // 6 - /// Initializes a new lending market obligation. + // 3 + /// Accrue interest and update market price of liquidity on a reserve. /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` Obligation account - uninitialized. - /// 1. `[]` Lending market account. - /// 2. `[signer]` Obligation owner. - /// 3. `[]` Clock sysvar. - /// 4. `[]` Rent sysvar. - /// 5. `[]` Token program id. - InitObligation, + /// 0. `[writable]` Reserve account. + /// 1. `[]` Clock sysvar. + /// 2. `[optional]` Reserve liquidity aggregator account. + /// Required if the reserve currency is not the lending market quote + /// currency. + RefreshReserve, // 4 /// Deposit liquidity into a reserve in exchange for collateral. Collateral represents a share @@ -118,6 +129,74 @@ pub enum LendingInstruction { collateral_amount: u64, }, + // 6 + /// Initializes a new lending market obligation. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation account - uninitialized. + /// 1. `[]` Lending market account. + /// 2. `[signer]` Obligation owner. + /// 3. `[]` Clock sysvar. + /// 4. `[]` Rent sysvar. + /// 5. `[]` Token program id. + InitObligation, + + // 7 + /// Refresh an obligation's accrued interest and collateral and liquidity prices. Requires + /// refreshed reserves, as all obligation collateral deposit reserves in order, followed by all + /// liquidity borrow reserves in order. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Obligation account. + /// 1. `[]` Clock sysvar. + /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. + /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. + RefreshObligation, + + // 8 + /// Deposit collateral to an obligation. Requires a refreshed reserve. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source collateral token account. + /// Minted by deposit reserve collateral mint. + /// $authority can transfer $collateral_amount. + /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. + /// 2. `[]` Deposit reserve account - refreshed. + /// 3. `[writable]` Obligation account. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[signer]` Obligation owner. + /// 7. `[signer]` User transfer authority ($authority). + /// 8. `[]` Clock sysvar. + /// 9. `[]` Token program id. + DepositObligationCollateral { + /// Amount of collateral tokens to deposit + collateral_amount: u64, + }, + + // 9 + /// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. + /// 1. `[writable]` Destination collateral token account. + /// Minted by withdraw reserve collateral mint. + /// 2. `[]` Withdraw reserve account - refreshed. + /// 3. `[writable]` Obligation account - refreshed. + /// 4. `[]` Lending market account. + /// 5. `[]` Derived lending market authority. + /// 6. `[signer]` Obligation owner. + /// 7. `[]` Clock sysvar. + /// 8. `[]` Token program id. + WithdrawObligationCollateral { + /// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount + collateral_amount: u64, + }, + // @TODO: rename cf. https://git.io/JOOE6 // 10 /// Borrow liquidity from a reserve by depositing collateral tokens. Requires a refreshed @@ -190,85 +269,6 @@ pub enum LendingInstruction { /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount liquidity_amount: u64, }, - - // 3 - /// Accrue interest and update market price of liquidity on a reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Reserve account. - /// 1. `[]` Clock sysvar. - /// 2. `[optional]` Reserve liquidity aggregator account. - /// Required if the reserve currency is not the lending market quote - /// currency. - RefreshReserve, - - // 8 - /// Deposit collateral to an obligation. Requires a refreshed reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Source collateral token account. - /// Minted by deposit reserve collateral mint. - /// $authority can transfer $collateral_amount. - /// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account. - /// 2. `[]` Deposit reserve account - refreshed. - /// 3. `[writable]` Obligation account. - /// 4. `[]` Lending market account. - /// 5. `[]` Derived lending market authority. - /// 6. `[signer]` Obligation owner. - /// 7. `[signer]` User transfer authority ($authority). - /// 8. `[]` Clock sysvar. - /// 9. `[]` Token program id. - DepositObligationCollateral { - /// Amount of collateral tokens to deposit - collateral_amount: u64, - }, - - // 9 - /// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account. - /// 1. `[writable]` Destination collateral token account. - /// Minted by withdraw reserve collateral mint. - /// 2. `[]` Withdraw reserve account - refreshed. - /// 3. `[writable]` Obligation account - refreshed. - /// 4. `[]` Lending market account. - /// 5. `[]` Derived lending market authority. - /// 6. `[signer]` Obligation owner. - /// 7. `[]` Clock sysvar. - /// 8. `[]` Token program id. - WithdrawObligationCollateral { - /// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount - collateral_amount: u64, - }, - - // 1 - /// Sets the new owner of a lending market. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Lending market account. - /// 1. `[signer]` Current owner. - SetLendingMarketOwner { - /// The new owner - new_owner: Pubkey, - }, - - // 7 - /// Refresh an obligation's accrued interest and collateral and liquidity prices. Requires - /// refreshed reserves, as all obligation collateral deposit reserves in order, followed by all - /// liquidity borrow reserves in order. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Obligation account. - /// 1. `[]` Clock sysvar. - /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. - /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. - RefreshObligation, } impl LendingInstruction { @@ -282,6 +282,10 @@ impl LendingInstruction { let (owner, _rest) = Self::unpack_pubkey(rest)?; Self::InitLendingMarket { owner } } + 1 => { + let (new_owner, _rest) = Self::unpack_pubkey(rest)?; + Self::SetLendingMarketOwner { new_owner } + } 2 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; @@ -310,7 +314,7 @@ impl LendingInstruction { }, } } - 6 => Self::InitObligation, + 3 => Self::RefreshReserve, 4 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::DepositReserveLiquidity { liquidity_amount } @@ -319,6 +323,16 @@ impl LendingInstruction { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; Self::RedeemReserveCollateral { collateral_amount } } + 6 => Self::InitObligation, + 7 => Self::RefreshObligation, + 8 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::DepositObligationCollateral { collateral_amount } + } + 9 => { + let (collateral_amount, _rest) = Self::unpack_u64(rest)?; + Self::WithdrawObligationCollateral { collateral_amount } + } 10 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::BorrowObligationLiquidity { liquidity_amount } @@ -331,20 +345,6 @@ impl LendingInstruction { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::LiquidateObligation { liquidity_amount } } - 3 => Self::RefreshReserve, - 8 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::DepositObligationCollateral { collateral_amount } - } - 9 => { - let (collateral_amount, _rest) = Self::unpack_u64(rest)?; - Self::WithdrawObligationCollateral { collateral_amount } - } - 1 => { - let (new_owner, _rest) = Self::unpack_pubkey(rest)?; - Self::SetLendingMarketOwner { new_owner } - } - 7 => Self::RefreshObligation, _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -398,6 +398,10 @@ impl LendingInstruction { buf.push(0); buf.extend_from_slice(owner.as_ref()); } + Self::SetLendingMarketOwner { new_owner } => { + buf.push(1); + buf.extend_from_slice(new_owner.as_ref()); + } Self::InitReserve { liquidity_amount, config: @@ -428,8 +432,8 @@ impl LendingInstruction { buf.extend_from_slice(&borrow_fee_wad.to_le_bytes()); buf.extend_from_slice(&host_fee_percentage.to_le_bytes()); } - Self::InitObligation => { - buf.push(6); + Self::RefreshReserve => { + buf.push(3); } Self::DepositReserveLiquidity { liquidity_amount } => { buf.push(4); @@ -439,20 +443,11 @@ impl LendingInstruction { buf.push(5); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::BorrowObligationLiquidity { liquidity_amount } => { - buf.push(10); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - } - Self::RepayObligationLiquidity { liquidity_amount } => { - buf.push(11); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); - } - Self::LiquidateObligation { liquidity_amount } => { - buf.push(12); - buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + Self::InitObligation => { + buf.push(6); } - Self::RefreshReserve => { - buf.push(3); + Self::RefreshObligation => { + buf.push(7); } Self::DepositObligationCollateral { collateral_amount } => { buf.push(8); @@ -462,12 +457,17 @@ impl LendingInstruction { buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::SetLendingMarketOwner { new_owner } => { - buf.push(1); - buf.extend_from_slice(new_owner.as_ref()); + Self::BorrowObligationLiquidity { liquidity_amount } => { + buf.push(10); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } - Self::RefreshObligation => { - buf.push(7); + Self::RepayObligationLiquidity { liquidity_amount } => { + buf.push(11); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + } + Self::LiquidateObligation { liquidity_amount } => { + buf.push(12); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } } buf @@ -496,15 +496,32 @@ pub fn init_lending_market( } } -/// Creates an 'InitReserve' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_reserve( +/// Creates a 'SetLendingMarketOwner' instruction. +pub fn set_lending_market_owner( program_id: Pubkey, - liquidity_amount: u64, - config: ReserveConfig, - source_liquidity_pubkey: Pubkey, - destination_collateral_pubkey: Pubkey, - reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + lending_market_owner: Pubkey, + new_owner: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_owner, true), + ], + data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), + } +} + +/// Creates an 'InitReserve' instruction. +#[allow(clippy::too_many_arguments)] +pub fn init_reserve( + program_id: Pubkey, + liquidity_amount: u64, + config: ReserveConfig, + source_liquidity_pubkey: Pubkey, + destination_collateral_pubkey: Pubkey, + reserve_pubkey: Pubkey, reserve_liquidity_mint_pubkey: Pubkey, reserve_liquidity_supply_pubkey: Pubkey, reserve_liquidity_fee_receiver_pubkey: Pubkey, @@ -553,25 +570,26 @@ pub fn init_reserve( } } -/// Creates an 'InitObligation' instruction. -#[allow(clippy::too_many_arguments)] -pub fn init_obligation( +/// Creates a `RefreshReserve` instruction +pub fn refresh_reserve( program_id: Pubkey, - obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - obligation_owner_pubkey: Pubkey, + reserve_pubkey: Pubkey, + reserve_liquidity_aggregator_pubkey: Option, ) -> Instruction { + let mut accounts = vec![ + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + accounts.push(AccountMeta::new_readonly( + reserve_liquidity_aggregator_pubkey, + false, + )); + } Instruction { program_id, - accounts: vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::InitObligation.pack(), + accounts, + data: LendingInstruction::RefreshReserve.pack(), } } @@ -645,133 +663,48 @@ pub fn redeem_reserve_collateral( } } -/// Creates a 'BorrowObligationLiquidity' instruction. +/// Creates an 'InitObligation' instruction. #[allow(clippy::too_many_arguments)] -pub fn borrow_obligation_liquidity( +pub fn init_obligation( program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - borrow_reserve_pubkey: Pubkey, - borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, - host_fee_receiver_pubkey: Option, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, - ); - let mut accounts = vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(borrow_reserve_pubkey, false), - AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ]; - if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { - accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); - } - Instruction { - program_id, - accounts, - data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), - } -} - -/// Creates a `RepayObligationLiquidity` instruction -#[allow(clippy::too_many_arguments)] -pub fn repay_obligation_liquidity( - program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, - obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, ) -> Instruction { Instruction { program_id, accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), + data: LendingInstruction::InitObligation.pack(), } } -/// Creates a `LiquidateObligation` instruction +/// Creates a 'RefreshObligation' instruction. #[allow(clippy::too_many_arguments)] -pub fn liquidate_obligation( +pub fn refresh_obligation( program_id: Pubkey, - liquidity_amount: u64, - source_liquidity_pubkey: Pubkey, - destination_collateral_pubkey: Pubkey, - repay_reserve_pubkey: Pubkey, - repay_reserve_liquidity_supply_pubkey: Pubkey, - withdraw_reserve_pubkey: Pubkey, - withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - user_transfer_authority_pubkey: Pubkey, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, - ); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), - AccountMeta::new(repay_reserve_pubkey, false), - AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new(withdraw_reserve_pubkey, false), - AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), - } -} - -/// Creates a `RefreshReserve` instruction -pub fn refresh_reserve( - program_id: Pubkey, - reserve_pubkey: Pubkey, - reserve_liquidity_aggregator_pubkey: Option, + reserve_pubkeys: Vec, ) -> Instruction { let mut accounts = vec![ - AccountMeta::new(reserve_pubkey, false), + AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; - if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { - accounts.push(AccountMeta::new_readonly( - reserve_liquidity_aggregator_pubkey, - false, - )); - } + accounts.extend( + reserve_pubkeys + .into_iter() + .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + ); Instruction { program_id, accounts, - data: LendingInstruction::RefreshReserve.pack(), + data: LendingInstruction::RefreshObligation.pack(), } } @@ -843,42 +776,109 @@ pub fn withdraw_obligation_collateral( } } -/// Creates a 'SetLendingMarketOwner' instruction. -pub fn set_lending_market_owner( +/// Creates a 'BorrowObligationLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn borrow_obligation_liquidity( program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + borrow_reserve_pubkey: Pubkey, + borrow_reserve_liquidity_fee_receiver_pubkey: Pubkey, + obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, - lending_market_owner: Pubkey, - new_owner: Pubkey, + obligation_owner_pubkey: Pubkey, + host_fee_receiver_pubkey: Option, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); + let mut accounts = vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(borrow_reserve_pubkey, false), + AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { + accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); + } + Instruction { + program_id, + accounts, + data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), + } +} + +/// Creates a `RepayObligationLiquidity` instruction +#[allow(clippy::too_many_arguments)] +pub fn repay_obligation_liquidity( + program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + obligation_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, ) -> Instruction { Instruction { program_id, accounts: vec![ - AccountMeta::new(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_owner, true), + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), ], - data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), + data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), } } -/// Creates a 'RefreshObligation' instruction. +/// Creates a `LiquidateObligation` instruction #[allow(clippy::too_many_arguments)] -pub fn refresh_obligation( +pub fn liquidate_obligation( program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_collateral_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + repay_reserve_liquidity_supply_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + withdraw_reserve_collateral_supply_pubkey: Pubkey, obligation_pubkey: Pubkey, - reserve_pubkeys: Vec, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, ) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - ]; - accounts.extend( - reserve_pubkeys - .into_iter() - .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, ); Instruction { program_id, - accounts, - data: LendingInstruction::RefreshObligation.pack(), + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index dc06e45a496..bb9e4197663 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -129,6 +129,36 @@ fn process_init_lending_market( Ok(()) } +#[inline(never)] // avoid stack frame limit +fn process_set_lending_market_owner( + program_id: &Pubkey, + new_owner: Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_owner_info = next_account_info(account_info_iter)?; + + let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); + return Err(LendingError::InvalidMarketOwner.into()); + } + if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } + + lending_market.owner = new_owner; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; + + Ok(()) +} + fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, @@ -344,46 +374,35 @@ fn process_init_reserve( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let obligation_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let obligation_owner_info = next_account_info(account_info_iter)?; +fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, obligation_info)?; - let mut obligation = assert_uninitialized::(obligation_info)?; - if obligation_info.owner != program_id { - msg!("Obligation provided is not owned by the lending program"); + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - msg!("Lending market provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &lending_market.token_program_id != token_program_id.key { - msg!("Lending market token program does not match the token program provided"); - return Err(LendingError::InvalidTokenProgram.into()); - } + if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { + let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; + if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { + msg!( + "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } - if !obligation_owner_info.is_signer { - msg!("Obligation owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); + reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; + } else if account_info_iter.peek().is_some() { + msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); + return Err(LendingError::InvalidAccountInput.into()); } - obligation.init(InitObligationParams { - current_slot: clock.slot, - lending_market: *lending_market_info.key, - owner: *obligation_owner_info.key, - deposits: vec![], - borrows: vec![], - }); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + reserve.accrue_interest(clock.slot)?; + reserve.last_update.update_slot(clock.slot); + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; Ok(()) } @@ -589,28 +608,22 @@ fn process_redeem_reserve_collateral( } #[inline(never)] // avoid stack frame limit -fn process_borrow_obligation_liquidity( - program_id: &Pubkey, - liquidity_amount: u64, - accounts: &[AccountInfo], -) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); - return Err(LendingError::InvalidAmount.into()); - } - +fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let borrow_reserve_info = next_account_info(account_info_iter)?; - let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, obligation_info)?; + let mut obligation = assert_uninitialized::(obligation_info)?; + if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); @@ -621,164 +634,169 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve_info.owner != program_id { - msg!("Borrow reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &borrow_reserve.lending_market != lending_market_info.key { - msg!("Borrow reserve lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { - msg!( - "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" - ); - return Err(LendingError::InvalidAccountInput.into()); - } - if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { - msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if borrow_reserve.last_update.is_stale(clock.slot)? { - msg!("Borrow reserve is stale and must be refreshed in the current slot"); - return Err(LendingError::ReserveStale.into()); + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); } + obligation.init(InitObligationParams { + current_slot: clock.slot, + lending_market: *lending_market_info.key, + owner: *obligation_owner_info.key, + deposits: vec![], + borrows: vec![], + }); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + + Ok(()) +} + +fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let obligation_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &obligation.lending_market != lending_market_info.key { - msg!("Obligation lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &obligation.owner != obligation_owner_info.key { - msg!("Obligation owner does not match the obligation owner provided"); - return Err(LendingError::InvalidObligationOwner.into()); - } - if !obligation_owner_info.is_signer { - msg!("Obligation owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); - } - if obligation.last_update.is_stale(clock.slot)? { - msg!("Obligation is stale and must be refreshed in the current slot"); - return Err(LendingError::ObligationStale.into()); - } - if obligation.deposits.is_empty() { - msg!("Obligation has no deposits to borrow against"); - return Err(LendingError::ObligationDepositsEmpty.into()); - } - if obligation.deposited_value == Decimal::zero() { - msg!("Obligation deposits have zero value"); - return Err(LendingError::ObligationDepositsZero.into()); - } - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if &lending_market_authority_pubkey != lending_market_authority_info.key { - msg!( - "Derived lending market authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidMarketAuthority.into()); - } + let mut deposited_value = Decimal::zero(); + let mut borrowed_value = Decimal::zero(); + let mut loan_to_value_ratio = Decimal::zero(); + let mut liquidation_threshold = Decimal::zero(); - let remaining_borrow_value = obligation.max_borrow_value()?; - if remaining_borrow_value == Decimal::zero() { - msg!("Remaining borrow value is zero"); - return Err(LendingError::BorrowTooLarge.into()); - } + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { + let deposit_reserve_info = next_account_info(account_info_iter)?; + if deposit_reserve_info.owner != program_id { + msg!( + "Deposit reserve provided for collateral {} is not owned by the lending program", + index + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if collateral.deposit_reserve != *deposit_reserve_info.key { + msg!( + "Deposit reserve of collateral {} does not match the deposit reserve provided", + index + ); + return Err(LendingError::InvalidAccountInput.into()); + } - let CalculateBorrowResult { - borrow_amount, - receive_amount, - borrow_fee, - host_fee, - } = borrow_reserve.calculate_borrow(liquidity_amount, remaining_borrow_value)?; + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", + index + ); + return Err(LendingError::ReserveStale.into()); + } - if receive_amount == 0 { - msg!("Borrow amount is too small to receive liquidity after fees"); - return Err(LendingError::BorrowTooSmall.into()); - } + let decimals = 10u64 + .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; - borrow_reserve - .liquidity - .borrow(borrow_amount, receive_amount)?; - borrow_reserve.last_update.mark_stale(); - Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; + let market_value = deposit_reserve + .collateral_exchange_rate()? + .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? + .try_mul(deposit_reserve.liquidity.market_price)? + .try_div(decimals)?; + collateral.market_value = market_value; - obligation - .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key)? - .borrow(borrow_amount)?; - obligation.last_update.mark_stale(); - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); + let liquidation_threshold_rate = + Rate::from_percent(deposit_reserve.config.liquidation_threshold); - let mut owner_fee = borrow_fee; - if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { - if host_fee > 0 { - owner_fee = owner_fee - .checked_sub(host_fee) - .ok_or(LendingError::MathOverflow)?; + deposited_value = deposited_value.try_add(market_value)?; + loan_to_value_ratio = + loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; + liquidation_threshold = + liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; + } - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: host_fee_receiver_info.clone(), - amount: host_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { + let borrow_reserve_info = next_account_info(account_info_iter)?; + if borrow_reserve_info.owner != program_id { + msg!( + "Borrow reserve provided for liquidity {} is not owned by the lending program", + index + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if liquidity.borrow_reserve != *borrow_reserve_info.key { + msg!( + "Borrow reserve of liquidity {} does not match the borrow reserve provided", + index + ); + return Err(LendingError::InvalidAccountInput.into()); + } + + let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!( + "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", + index + ); + return Err(LendingError::ReserveStale.into()); } + + let decimals = 10u64 + .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + + liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; + let market_value = liquidity + .borrowed_amount_wads + .try_mul(borrow_reserve.liquidity.market_price)? + .try_div(decimals)?; + liquidity.market_value = market_value; + borrowed_value = borrowed_value.try_add(market_value)?; } - if owner_fee > 0 { - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: borrow_reserve_liquidity_fee_receiver_info.clone(), - amount: owner_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + + if account_info_iter.peek().is_some() { + msg!("Too many obligation deposit or borrow reserves provided"); + return Err(LendingError::InvalidAccountInput.into()); } - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: receive_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + obligation.deposited_value = deposited_value; + obligation.borrowed_value = borrowed_value; + + if deposited_value == Decimal::zero() { + obligation.loan_to_value_ratio = Rate::zero(); + obligation.liquidation_threshold = Rate::zero(); + } else { + obligation.loan_to_value_ratio = + Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; + obligation.liquidation_threshold = + Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; + } + + obligation.last_update.update_slot(clock.slot); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } #[inline(never)] // avoid stack frame limit -fn process_repay_obligation_liquidity( +fn process_deposit_obligation_collateral( program_id: &Pubkey, - liquidity_amount: u64, + collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); + if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; + let source_collateral_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let deposit_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -793,27 +811,33 @@ fn process_repay_obligation_liquidity( return Err(LendingError::InvalidTokenProgram.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { - msg!("Repay reserve provided is not owned by the lending program"); + let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + if deposit_reserve_info.owner != program_id { + msg!("Deposit reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &repay_reserve.lending_market != lending_market_info.key { - msg!("Repay reserve lending market does not match the lending market provided"); + if &deposit_reserve.lending_market != lending_market_info.key { + msg!("Deposit reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); + if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { + msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { - msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); + if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { + msg!( + "Deposit reserve collateral supply must be used as the destination collateral provided" + ); return Err(LendingError::InvalidAccountInput.into()); } - if repay_reserve.last_update.is_stale(clock.slot)? { - msg!("Repay reserve is stale and must be refreshed in the current slot"); + if deposit_reserve.last_update.is_stale(clock.slot)? { + msg!("Deposit reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } + if deposit_reserve.config.loan_to_value_ratio == 0 { + msg!("Deposit reserve has collateral disabled for borrowing"); + return Err(LendingError::ReserveCollateralDisabled.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -824,40 +848,38 @@ fn process_repay_obligation_liquidity( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if obligation.last_update.is_stale(clock.slot)? { - msg!("Obligation is stale and must be refreshed in the current slot"); - return Err(LendingError::ObligationStale.into()); + if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); + return Err(LendingError::InvalidObligationOwner.into()); } - - let (liquidity, liquidity_index) = - obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; - if liquidity.borrowed_amount_wads == Decimal::zero() { - msg!("Liquidity borrowed amount is zero"); - return Err(LendingError::ObligationLiquidityEmpty.into()); + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); } - let CalculateRepayResult { - settle_amount, - repay_amount, - } = repay_reserve.calculate_repay(liquidity_amount, liquidity.borrowed_amount_wads)?; - - if repay_amount == 0 { - msg!("Repay amount is too small to transfer liquidity"); - return Err(LendingError::RepayTooSmall.into()); + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); } - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - repay_reserve.last_update.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - - obligation.repay(settle_amount, liquidity_index)?; + obligation + .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? + .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: repay_amount, + source: source_collateral_info.clone(), + destination: destination_collateral_info.clone(), + amount: collateral_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), @@ -867,27 +889,24 @@ fn process_repay_obligation_liquidity( } #[inline(never)] // avoid stack frame limit -fn process_liquidate_obligation( +fn process_withdraw_obligation_collateral( program_id: &Pubkey, - liquidity_amount: u64, + collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); + if collateral_amount == 0 { + msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; + let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; - let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -901,34 +920,6 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidTokenProgram.into()); } - let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; - if repay_reserve_info.owner != program_id { - msg!("Repay reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - if &repay_reserve.lending_market != lending_market_info.key { - msg!("Repay reserve lending market does not match the lending market provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { - msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!( - "Repay reserve collateral supply cannot be used as the destination collateral provided" - ); - return Err(LendingError::InvalidAccountInput.into()); - } - if repay_reserve.last_update.is_stale(clock.slot)? { - msg!("Repay reserve is stale and must be refreshed in the current slot"); - return Err(LendingError::ReserveStale.into()); - } - let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { msg!("Withdraw reserve provided is not owned by the lending program"); @@ -938,12 +929,8 @@ fn process_liquidate_obligation( msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { - msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); - return Err(LendingError::InvalidAccountInput.into()); - } - if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { - msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); + if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { + msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { @@ -964,28 +951,26 @@ fn process_liquidate_obligation( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } + if &obligation.owner != obligation_owner_info.key { + msg!("Obligation owner does not match the obligation owner provided"); + return Err(LendingError::InvalidObligationOwner.into()); + } + if !obligation_owner_info.is_signer { + msg!("Obligation owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } - if obligation.deposited_value == Decimal::zero() { - msg!("Obligation deposited value is zero"); - return Err(LendingError::ObligationDepositsZero.into()); - } - if obligation.borrowed_value == Decimal::zero() { - msg!("Obligation borrowed value is zero"); - return Err(LendingError::ObligationBorrowsZero.into()); - } - if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { - msg!("Obligation is healthy and cannot be liquidated"); - return Err(LendingError::ObligationHealthy.into()); - } - let (liquidity, liquidity_index) = - obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - + if collateral.deposited_amount == 0 { + msg!("Collateral deposited amount is zero"); + return Err(LendingError::ObligationCollateralEmpty.into()); + } + let authority_signer_seeds = &[ lending_market_info.key.as_ref(), &[lending_market.bump_seed], @@ -999,46 +984,56 @@ fn process_liquidate_obligation( return Err(LendingError::InvalidMarketAuthority.into()); } - let CalculateLiquidationResult { - settle_amount, - repay_amount, - withdraw_amount, - } = withdraw_reserve.calculate_liquidation( - liquidity_amount, - &obligation, - &liquidity, - &collateral, - )?; - - if repay_amount == 0 { - msg!("Liquidation is too small to transfer liquidity"); - return Err(LendingError::LiquidationTooSmall.into()); - } - if withdraw_amount == 0 { - msg!("Liquidation is too small to receive collateral"); - return Err(LendingError::LiquidationTooSmall.into()); - } + let withdraw_amount = if obligation.borrows.is_empty() { + // there are no borrows; they have been repaid, liquidated, or were never taken out + if collateral_amount == u64::MAX { + collateral.deposited_amount + } else { + collateral.deposited_amount.min(collateral_amount) + } + } else if obligation.deposited_value == Decimal::zero() { + // there are deposits, but they cannot be valued + msg!("Obligation deposited value is zero"); + return Err(LendingError::ObligationDepositsZero.into()); + } else { + // there are borrows and deposits, and they can both be valued + let max_withdraw_value = obligation.max_withdraw_value()?; + if max_withdraw_value == Decimal::zero() { + msg!("Maximum withdraw value is zero"); + return Err(LendingError::WithdrawTooLarge.into()); + } - repay_reserve.liquidity.repay(repay_amount, settle_amount)?; - repay_reserve.last_update.mark_stale(); - Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + let withdraw_amount = if collateral_amount == u64::MAX { + let withdraw_value = max_withdraw_value.min(collateral.market_value); + let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; + withdraw_pct + .try_mul(collateral.deposited_amount)? + .try_floor_u64()? + .min(collateral.deposited_amount) + } else { + let withdraw_amount = collateral_amount.min(collateral.deposited_amount); + let withdraw_pct = + Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; + let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; + if withdraw_value > max_withdraw_value { + msg!("Withdraw value cannot exceed maximum withdraw value"); + return Err(LendingError::WithdrawTooLarge.into()); + } + withdraw_amount + }; + if withdraw_amount == 0 { + msg!("Withdraw amount is too small to transfer collateral"); + return Err(LendingError::WithdrawTooSmall.into()); + } + withdraw_amount + }; - obligation.repay(settle_amount, liquidity_index)?; obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: repay_reserve_liquidity_supply_info.clone(), - amount: repay_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], - token_program: token_program_id.clone(), - })?; - - spl_token_transfer(TokenTransferParams { - source: withdraw_reserve_collateral_supply_info.clone(), + source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), amount: withdraw_amount, authority: lending_market_authority_info.clone(), @@ -1049,59 +1044,26 @@ fn process_liquidate_obligation( Ok(()) } -fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter().peekable(); - let reserve_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - msg!("Reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); - } - - if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { - let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; - if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { - msg!( - "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" - ); - return Err(LendingError::InvalidAccountInput.into()); - } - - reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; - } else if account_info_iter.peek().is_some() { - msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); - return Err(LendingError::InvalidAccountInput.into()); - } - - reserve.accrue_interest(clock.slot)?; - reserve.last_update.update_slot(clock.slot); - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; - - Ok(()) -} - #[inline(never)] // avoid stack frame limit -fn process_deposit_obligation_collateral( +fn process_borrow_obligation_liquidity( program_id: &Pubkey, - collateral_amount: u64, + liquidity_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if collateral_amount == 0 { - msg!("Collateral amount provided cannot be zero"); + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_collateral_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let deposit_reserve_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let borrow_reserve_info = next_account_info(account_info_iter)?; + let borrow_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1115,32 +1077,32 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidTokenProgram.into()); } - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve_info.owner != program_id { - msg!("Deposit reserve provided is not owned by the lending program"); + let mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; + if borrow_reserve_info.owner != program_id { + msg!("Borrow reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &deposit_reserve.lending_market != lending_market_info.key { - msg!("Deposit reserve lending market does not match the lending market provided"); + if &borrow_reserve.lending_market != lending_market_info.key { + msg!("Borrow reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { - msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); + if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { + msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { + if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { msg!( - "Deposit reserve collateral supply must be used as the destination collateral provided" + "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" ); return Err(LendingError::InvalidAccountInput.into()); } - if deposit_reserve.last_update.is_stale(clock.slot)? { - msg!("Deposit reserve is stale and must be refreshed in the current slot"); - return Err(LendingError::ReserveStale.into()); + if &borrow_reserve.liquidity.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { + msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); } - if deposit_reserve.config.loan_to_value_ratio == 0 { - msg!("Deposit reserve has collateral disabled for borrowing"); - return Err(LendingError::ReserveCollateralDisabled.into()); + if borrow_reserve.last_update.is_stale(clock.slot)? { + msg!("Borrow reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; @@ -1160,6 +1122,18 @@ fn process_deposit_obligation_collateral( msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); + return Err(LendingError::ObligationStale.into()); + } + if obligation.deposits.is_empty() { + msg!("Obligation has no deposits to borrow against"); + return Err(LendingError::ObligationDepositsEmpty.into()); + } + if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposits have zero value"); + return Err(LendingError::ObligationDepositsZero.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -1174,18 +1148,70 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidMarketAuthority.into()); } + let remaining_borrow_value = obligation.max_borrow_value()?; + if remaining_borrow_value == Decimal::zero() { + msg!("Remaining borrow value is zero"); + return Err(LendingError::BorrowTooLarge.into()); + } + + let CalculateBorrowResult { + borrow_amount, + receive_amount, + borrow_fee, + host_fee, + } = borrow_reserve.calculate_borrow(liquidity_amount, remaining_borrow_value)?; + + if receive_amount == 0 { + msg!("Borrow amount is too small to receive liquidity after fees"); + return Err(LendingError::BorrowTooSmall.into()); + } + + borrow_reserve + .liquidity + .borrow(borrow_amount, receive_amount)?; + borrow_reserve.last_update.mark_stale(); + Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; + obligation - .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? - .deposit(collateral_amount)?; + .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key)? + .borrow(borrow_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + let mut owner_fee = borrow_fee; + if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { + if host_fee > 0 { + owner_fee = owner_fee + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: host_fee_receiver_info.clone(), + amount: host_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + } + } + if owner_fee > 0 { + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: borrow_reserve_liquidity_fee_receiver_info.clone(), + amount: owner_fee, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + } + spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), - destination: destination_collateral_info.clone(), - amount: collateral_amount, - authority: user_transfer_authority_info.clone(), - authority_signer_seeds: &[], + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: receive_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, token_program: token_program_id.clone(), })?; @@ -1193,24 +1219,23 @@ fn process_deposit_obligation_collateral( } #[inline(never)] // avoid stack frame limit -fn process_withdraw_obligation_collateral( +fn process_repay_obligation_liquidity( program_id: &Pubkey, - collateral_amount: u64, + liquidity_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if collateral_amount == 0 { - msg!("Collateral amount provided cannot be zero"); + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); - let source_collateral_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let withdraw_reserve_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let obligation_owner_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1224,25 +1249,25 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidTokenProgram.into()); } - let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; - if withdraw_reserve_info.owner != program_id { - msg!("Withdraw reserve provided is not owned by the lending program"); + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &withdraw_reserve.lending_market != lending_market_info.key { - msg!("Withdraw reserve lending market does not match the lending market provided"); + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { - msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { - msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); + if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { + msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } - if withdraw_reserve.last_update.is_stale(clock.slot)? { - msg!("Withdraw reserve is stale and must be refreshed in the current slot"); + if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } @@ -1255,93 +1280,42 @@ fn process_withdraw_obligation_collateral( msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - if &obligation.owner != obligation_owner_info.key { - msg!("Obligation owner does not match the obligation owner provided"); - return Err(LendingError::InvalidObligationOwner.into()); - } - if !obligation_owner_info.is_signer { - msg!("Obligation owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); - } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } - let (collateral, collateral_index) = - obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - if collateral.deposited_amount == 0 { - msg!("Collateral deposited amount is zero"); - return Err(LendingError::ObligationCollateralEmpty.into()); + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + if liquidity.borrowed_amount_wads == Decimal::zero() { + msg!("Liquidity borrowed amount is zero"); + return Err(LendingError::ObligationLiquidityEmpty.into()); } - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if &lending_market_authority_pubkey != lending_market_authority_info.key { - msg!( - "Derived lending market authority does not match the lending market authority provided" - ); - return Err(LendingError::InvalidMarketAuthority.into()); - } + let CalculateRepayResult { + settle_amount, + repay_amount, + } = repay_reserve.calculate_repay(liquidity_amount, liquidity.borrowed_amount_wads)?; - let withdraw_amount = if obligation.borrows.is_empty() { - // there are no borrows; they have been repaid, liquidated, or were never taken out - if collateral_amount == u64::MAX { - collateral.deposited_amount - } else { - collateral.deposited_amount.min(collateral_amount) - } - } else if obligation.deposited_value == Decimal::zero() { - // there are deposits, but they cannot be valued - msg!("Obligation deposited value is zero"); - return Err(LendingError::ObligationDepositsZero.into()); - } else { - // there are borrows and deposits, and they can both be valued - let max_withdraw_value = obligation.max_withdraw_value()?; - if max_withdraw_value == Decimal::zero() { - msg!("Maximum withdraw value is zero"); - return Err(LendingError::WithdrawTooLarge.into()); - } + if repay_amount == 0 { + msg!("Repay amount is too small to transfer liquidity"); + return Err(LendingError::RepayTooSmall.into()); + } - let withdraw_amount = if collateral_amount == u64::MAX { - let withdraw_value = max_withdraw_value.min(collateral.market_value); - let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; - withdraw_pct - .try_mul(collateral.deposited_amount)? - .try_floor_u64()? - .min(collateral.deposited_amount) - } else { - let withdraw_amount = collateral_amount.min(collateral.deposited_amount); - let withdraw_pct = - Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - msg!("Withdraw value cannot exceed maximum withdraw value"); - return Err(LendingError::WithdrawTooLarge.into()); - } - withdraw_amount - }; - if withdraw_amount == 0 { - msg!("Withdraw amount is too small to transfer collateral"); - return Err(LendingError::WithdrawTooSmall.into()); - } - withdraw_amount - }; + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - obligation.withdraw(withdraw_amount, collateral_index)?; + obligation.repay(settle_amount, liquidity_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { - source: source_collateral_info.clone(), - destination: destination_collateral_info.clone(), - amount: withdraw_amount, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: repay_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; @@ -1349,159 +1323,185 @@ fn process_withdraw_obligation_collateral( } #[inline(never)] // avoid stack frame limit -fn process_set_lending_market_owner( +fn process_liquidate_obligation( program_id: &Pubkey, - new_owner: Pubkey, + liquidity_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); + return Err(LendingError::InvalidAmount.into()); + } + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; + let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let withdraw_reserve_info = next_account_info(account_info_iter)?; + let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; + let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_owner_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let token_program_id = next_account_info(account_info_iter)?; - let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - if &lending_market.owner != lending_market_owner_info.key { - msg!("Lending market owner does not match the lending market owner provided"); - return Err(LendingError::InvalidMarketOwner.into()); - } - if !lending_market_owner_info.is_signer { - msg!("Lending market owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); } - lending_market.owner = new_owner; - LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; - - Ok(()) -} + let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; + if repay_reserve_info.owner != program_id { + msg!("Repay reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &repay_reserve.lending_market != lending_market_info.key { + msg!("Repay reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { + msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!( + "Repay reserve collateral supply cannot be used as the destination collateral provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } + if repay_reserve.last_update.is_stale(clock.slot)? { + msg!("Repay reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); + } -fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter().peekable(); - let obligation_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; + if withdraw_reserve_info.owner != program_id { + msg!("Withdraw reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &withdraw_reserve.lending_market != lending_market_info.key { + msg!("Withdraw reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { + msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { + msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if withdraw_reserve.last_update.is_stale(clock.slot)? { + msg!("Withdraw reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } - - let mut deposited_value = Decimal::zero(); - let mut borrowed_value = Decimal::zero(); - let mut loan_to_value_ratio = Decimal::zero(); - let mut liquidation_threshold = Decimal::zero(); - - for (index, collateral) in obligation.deposits.iter_mut().enumerate() { - let deposit_reserve_info = next_account_info(account_info_iter)?; - if deposit_reserve_info.owner != program_id { - msg!( - "Deposit reserve provided for collateral {} is not owned by the lending program", - index - ); - return Err(LendingError::InvalidAccountOwner.into()); - } - if collateral.deposit_reserve != *deposit_reserve_info.key { - msg!( - "Deposit reserve of collateral {} does not match the deposit reserve provided", - index - ); - return Err(LendingError::InvalidAccountInput.into()); - } - - let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - if deposit_reserve.last_update.is_stale(clock.slot)? { - msg!( - "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", - index - ); - return Err(LendingError::ReserveStale.into()); - } - - let decimals = 10u64 - .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; - - let market_value = deposit_reserve - .collateral_exchange_rate()? - .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? - .try_mul(deposit_reserve.liquidity.market_price)? - .try_div(decimals)?; - collateral.market_value = market_value; - - let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); - let liquidation_threshold_rate = - Rate::from_percent(deposit_reserve.config.liquidation_threshold); - - deposited_value = deposited_value.try_add(market_value)?; - loan_to_value_ratio = - loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; - liquidation_threshold = - liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; + if &obligation.lending_market != lending_market_info.key { + msg!("Obligation lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed in the current slot"); + return Err(LendingError::ObligationStale.into()); + } + if obligation.deposited_value == Decimal::zero() { + msg!("Obligation deposited value is zero"); + return Err(LendingError::ObligationDepositsZero.into()); + } + if obligation.borrowed_value == Decimal::zero() { + msg!("Obligation borrowed value is zero"); + return Err(LendingError::ObligationBorrowsZero.into()); + } + if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { + msg!("Obligation is healthy and cannot be liquidated"); + return Err(LendingError::ObligationHealthy.into()); } - for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { - let borrow_reserve_info = next_account_info(account_info_iter)?; - if borrow_reserve_info.owner != program_id { - msg!( - "Borrow reserve provided for liquidity {} is not owned by the lending program", - index - ); - return Err(LendingError::InvalidAccountOwner.into()); - } - if liquidity.borrow_reserve != *borrow_reserve_info.key { - msg!( - "Borrow reserve of liquidity {} does not match the borrow reserve provided", - index - ); - return Err(LendingError::InvalidAccountInput.into()); - } + let (liquidity, liquidity_index) = + obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + let (collateral, collateral_index) = + obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; - let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; - if borrow_reserve.last_update.is_stale(clock.slot)? { - msg!( - "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", - index - ); - return Err(LendingError::ReserveStale.into()); - } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); + } - let decimals = 10u64 - .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; + let CalculateLiquidationResult { + settle_amount, + repay_amount, + withdraw_amount, + } = withdraw_reserve.calculate_liquidation( + liquidity_amount, + &obligation, + &liquidity, + &collateral, + )?; - liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; - let market_value = liquidity - .borrowed_amount_wads - .try_mul(borrow_reserve.liquidity.market_price)? - .try_div(decimals)?; - liquidity.market_value = market_value; - borrowed_value = borrowed_value.try_add(market_value)?; + if repay_amount == 0 { + msg!("Liquidation is too small to transfer liquidity"); + return Err(LendingError::LiquidationTooSmall.into()); } - - if account_info_iter.peek().is_some() { - msg!("Too many obligation deposit or borrow reserves provided"); - return Err(LendingError::InvalidAccountInput.into()); + if withdraw_amount == 0 { + msg!("Liquidation is too small to receive collateral"); + return Err(LendingError::LiquidationTooSmall.into()); } - obligation.deposited_value = deposited_value; - obligation.borrowed_value = borrowed_value; - - if deposited_value == Decimal::zero() { - obligation.loan_to_value_ratio = Rate::zero(); - obligation.liquidation_threshold = Rate::zero(); - } else { - obligation.loan_to_value_ratio = - Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; - obligation.liquidation_threshold = - Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; - } + repay_reserve.liquidity.repay(repay_amount, settle_amount)?; + repay_reserve.last_update.mark_stale(); + Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - obligation.last_update.update_slot(clock.slot); + obligation.repay(settle_amount, liquidity_index)?; + obligation.withdraw(withdraw_amount, collateral_index)?; + obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: repay_reserve_liquidity_supply_info.clone(), + amount: repay_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + + spl_token_transfer(TokenTransferParams { + source: withdraw_reserve_collateral_supply_info.clone(), + destination: destination_collateral_info.clone(), + amount: withdraw_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + Ok(()) } From 671affbe96d6489269455df6d4a6412fdc13a2e0 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 15:21:20 -0500 Subject: [PATCH 166/191] reorganize state packing --- .../program/src/state/lending_market.rs | 28 +- token-lending/program/src/state/obligation.rs | 151 +++++------ token-lending/program/src/state/reserve.rs | 248 +++++++++--------- 3 files changed, 219 insertions(+), 208 deletions(-) diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index b747388f94d..e6c14332240 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -65,12 +65,26 @@ const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128 impl Pack for LendingMarket { const LEN: usize = LENDING_MARKET_LEN; - /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html). + 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_token_mint, token_program_id, _padding) = + mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; + + *version = self.version.to_le_bytes(); + *bump_seed = self.bump_seed.to_le_bytes(); + owner.copy_from_slice(self.owner.as_ref()); + quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); + token_program_id.copy_from_slice(self.token_program_id.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_token_mint, token_program_id, _padding) = array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; + let version = u8::from_le_bytes(*version); if version > PROGRAM_VERSION { msg!("Lending market version does not match lending program version"); @@ -85,16 +99,4 @@ impl Pack for LendingMarket { token_program_id: Pubkey::new_from_array(*token_program_id), }) } - - 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_token_mint, token_program_id, _padding) = - mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128]; - *version = self.version.to_le_bytes(); - *bump_seed = self.bump_seed.to_le_bytes(); - owner.copy_from_slice(self.owner.as_ref()); - quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref()); - token_program_id.copy_from_slice(self.token_program_id.as_ref()); - } } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 3959f87c96c..1f082306b4a 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -325,8 +325,6 @@ impl ObligationLiquidity { } } -// @TODO: Adjust padding, but what's a reasonable number? -// Or should there be no padding to save space, but we need account resizing implemented? const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9) @@ -334,6 +332,83 @@ const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 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, + loan_to_value_ratio, + liquidation_threshold, + deposits_len, + borrows_len, + data_flat, + ) = mut_array_refs![ + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 16, + 16, + 16, + 16, + 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_rate(self.loan_to_value_ratio, loan_to_value_ratio); + pack_rate(self.liquidation_threshold, liquidation_threshold); + *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) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; + 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) = + mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; + 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)] @@ -365,6 +440,7 @@ impl Pack for Obligation { 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"); @@ -419,75 +495,4 @@ impl Pack for Obligation { liquidation_threshold: unpack_rate(liquidation_threshold), }) } - - 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, - loan_to_value_ratio, - liquidation_threshold, - deposits_len, - borrows_len, - data_flat, - ) = mut_array_refs![ - output, - 1, - 8, - 1, - PUBKEY_BYTES, - PUBKEY_BYTES, - 16, - 16, - 16, - 16, - 1, - 1, - OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) - ]; - - *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_rate(self.loan_to_value_ratio, loan_to_value_ratio); - pack_rate(self.liquidation_threshold, liquidation_threshold); - *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; - 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) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16]; - 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; - } - 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) = - mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16]; - 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; - } - } } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 34151d19449..78bfe23b793 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -358,14 +358,14 @@ pub struct ReserveLiquidity { pub fee_receiver: Pubkey, /// Optional reserve liquidity aggregator state account pub aggregator: COption, - /// Reserve liquidity cumulative borrow rate - pub cumulative_borrow_rate_wads: Decimal, - /// Reserve liquidity market price in quote currency - pub market_price: u64, /// 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 liquidity market price in quote currency + pub market_price: u64, } impl ReserveLiquidity { @@ -689,25 +689,30 @@ impl IsInitialized for Reserve { } const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 +// @TODO: break this up by reserve / liquidity / collateral / config impl Pack for Reserve { const LEN: usize = RESERVE_LEN; - /// 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]; + 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, + liquidity_mint_pubkey, liquidity_mint_decimals, - liquidity_supply, - collateral_mint, - collateral_supply, + liquidity_supply_pubkey, liquidity_fee_receiver, liquidity_aggregator, + liquidity_available_amount, + liquidity_borrowed_amount_wads, + liquidity_cumulative_borrow_rate_wads, + liquidity_market_price, + collateral_mint_pubkey, + collateral_mint_amount, + collateral_supply_pubkey, config_optimal_utilization_rate, config_loan_to_value_ratio, config_liquidation_bonus, @@ -717,14 +722,9 @@ impl Pack for Reserve { config_max_borrow_rate, config_fees_borrow_fee_wad, config_fees_host_fee_percentage, - liquidity_cumulative_borrow_rate_wads, - liquidity_borrowed_amount_wads, - liquidity_available_amount, - collateral_mint_amount, - liquidity_market_price, _padding, - ) = array_refs![ - input, + ) = mut_array_refs![ + output, 1, 8, 1, @@ -733,9 +733,14 @@ impl Pack for Reserve { 1, PUBKEY_BYTES, PUBKEY_BYTES, + 4 + PUBKEY_BYTES, + 8, + 16, + 16, + 8, PUBKEY_BYTES, + 8, PUBKEY_BYTES, - 4 + PUBKEY_BYTES, 1, 1, 1, @@ -745,73 +750,70 @@ impl Pack for Reserve { 1, 8, 1, - 16, - 16, - 8, - 8, - 8, 256 ]; - let version = u8::from_le_bytes(*version); - if version > PROGRAM_VERSION { - msg!("Reserve version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } - 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), - mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), - supply_pubkey: Pubkey::new_from_array(*liquidity_supply), - available_amount: u64::from_le_bytes(*liquidity_available_amount), - borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), - aggregator: unpack_coption_key(liquidity_aggregator)?, - market_price: u64::from_le_bytes(*liquidity_market_price), - cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), - fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), - }, - collateral: ReserveCollateral { - mint_pubkey: Pubkey::new_from_array(*collateral_mint), - mint_amount: u64::from_le_bytes(*collateral_mint_amount), - supply_pubkey: Pubkey::new_from_array(*collateral_supply), - }, - config: ReserveConfig { - optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate), - loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), - liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus), - liquidation_threshold: u8::from_le_bytes(*config_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: u8::from_le_bytes(*config_max_borrow_rate), - fees: ReserveFees { - borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), - host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), - }, - }, - }) + + // 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_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); + pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); + *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, + ); + *liquidity_market_price = self.liquidity.market_price.to_le_bytes(); + + // collateral + collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref()); + *collateral_mint_amount = self.collateral.mint_amount.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_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_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); + *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); } - // @TODO: break this up by reserve / liquidity / collateral / config - fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN]; + /// 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 ( + let ( version, last_update_slot, last_update_stale, lending_market, - liquidity_mint, + liquidity_mint_pubkey, liquidity_mint_decimals, - liquidity_supply, - collateral_mint, - collateral_supply, + liquidity_supply_pubkey, liquidity_fee_receiver, liquidity_aggregator, + liquidity_available_amount, + liquidity_borrowed_amount_wads, + liquidity_cumulative_borrow_rate_wads, + liquidity_market_price, + collateral_mint_pubkey, + collateral_mint_amount, + collateral_supply_pubkey, config_optimal_utilization_rate, config_loan_to_value_ratio, config_liquidation_bonus, @@ -821,14 +823,9 @@ impl Pack for Reserve { config_max_borrow_rate, config_fees_borrow_fee_wad, config_fees_host_fee_percentage, - liquidity_cumulative_borrow_rate_wads, - liquidity_borrowed_amount_wads, - liquidity_available_amount, - collateral_mint_amount, - liquidity_market_price, _padding, - ) = mut_array_refs![ - output, + ) = array_refs![ + input, 1, 8, 1, @@ -837,9 +834,14 @@ impl Pack for Reserve { 1, PUBKEY_BYTES, PUBKEY_BYTES, + 4 + PUBKEY_BYTES, + 8, + 16, + 16, + 8, PUBKEY_BYTES, + 8, PUBKEY_BYTES, - 4 + PUBKEY_BYTES, 1, 1, 1, @@ -849,49 +851,51 @@ impl Pack for Reserve { 1, 8, 1, - 16, - 16, - 8, - 8, - 8, 256 ]; - *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); - pack_decimal( - self.liquidity.cumulative_borrow_rate_wads, - liquidity_cumulative_borrow_rate_wads, - ); - lending_market.copy_from_slice(self.lending_market.as_ref()); - pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); - *liquidity_market_price = self.liquidity.market_price.to_le_bytes(); - - // liquidity info - liquidity_mint.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); - *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); - liquidity_supply.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); - *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); - pack_decimal( - self.liquidity.borrowed_amount_wads, - liquidity_borrowed_amount_wads, - ); - // collateral info - collateral_mint.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - *collateral_mint_amount = self.collateral.mint_amount.to_le_bytes(); - collateral_supply.copy_from_slice(self.collateral.supply_pubkey.as_ref()); - liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); + let version = u8::from_le_bytes(*version); + if version > PROGRAM_VERSION { + msg!("Reserve version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } - // config - *config_optimal_utilization_rate = self.config.optimal_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_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); - *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); + 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), + fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), + aggregator: unpack_coption_key(liquidity_aggregator)?, + 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), + market_price: u64::from_le_bytes(*liquidity_market_price), + }, + collateral: ReserveCollateral { + mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), + mint_amount: u64::from_le_bytes(*collateral_mint_amount), + supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey), + }, + config: ReserveConfig { + optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate), + loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), + liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus), + liquidation_threshold: u8::from_le_bytes(*config_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: u8::from_le_bytes(*config_max_borrow_rate), + fees: ReserveFees { + borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), + host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), + }, + }, + }) } } From ae22a7446b4d56795fca7ebb9d20a091e18ba511 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 16:00:02 -0500 Subject: [PATCH 167/191] add quote mint decimals check for aggregator --- token-lending/program/src/error.rs | 3 +++ token-lending/program/src/instruction.rs | 14 +++++----- token-lending/program/src/processor.rs | 33 ++++++++++++++++++------ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index daab737a949..b5ba3d39dab 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -147,6 +147,9 @@ pub enum LendingError { /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, + /// Aggregator config is invalid + #[error("Input aggregator config is invalid")] + InvalidAggregatorConfig, } impl From for ProgramError { diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 48d305d3cb9..2ae411bd7ac 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -56,17 +56,17 @@ pub enum LendingInstruction { /// 5. `[writable]` Reserve liquidity fee receiver - uninitialized. /// 6. `[writable]` Reserve collateral SPL Token mint - uninitialized. /// 7. `[writable]` Reserve collateral token supply - uninitialized. - /// 8. `[]` Lending market account. + /// 8. `[]` Quote currency SPL Token mint. + /// 9. `[]` Lending market account. /// 10 `[]` Derived lending market authority. - /// 11 `[]` User transfer authority ($authority). - // @FIXME: lending market owner is only required because of the trusted aggregator - /// 12 `[signer]` Lending market owner. + /// 11 `[signer]` Lending market owner. + /// 12 `[]` User transfer authority ($authority). /// 13 `[]` Clock sysvar. /// 13 `[]` Rent sysvar. /// 14 `[]` Token program id. /// 15 `[optional]` Reserve liquidity aggregator account. /// Not required for quote currency reserves. - /// Must match quote and base currency. + /// Must match base and quote currency mint, and quote currency decimals. InitReserve { /// Initial amount of liquidity to deposit into the new reserve liquidity_amount: u64, @@ -527,6 +527,7 @@ pub fn init_reserve( reserve_liquidity_fee_receiver_pubkey: Pubkey, reserve_collateral_mint_pubkey: Pubkey, reserve_collateral_supply_pubkey: Pubkey, + quote_token_mint_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, @@ -545,9 +546,10 @@ pub fn init_reserve( AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(reserve_collateral_mint_pubkey, false), AccountMeta::new(reserve_collateral_supply_pubkey, false), + AccountMeta::new_readonly(quote_token_mint_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_owner_pubkey, true), AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(lending_market_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index bb9e4197663..43d7b299e8c 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -11,7 +11,11 @@ use crate::{ ReserveCollateral, ReserveConfig, ReserveLiquidity, }, }; -use flux_aggregator::read_median; +use flux_aggregator::{ + borsh_state::InitBorshState, + read_median, + state::Aggregator +}; use num_traits::FromPrimitive; use solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -213,10 +217,10 @@ fn process_init_reserve( let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_collateral_supply_info = next_account_info(account_info_iter)?; + let quote_token_mint_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - // @FIXME: lending market owner is only required because of the trusted aggregator - let lending_market_owner_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; + let lending_market_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; @@ -242,6 +246,12 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } + let quote_token_mint = unpack_mint("e_token_mint_info.data.borrow())?; + if quote_token_mint_info.owner != token_program_id.key { + msg!("Quote token mint provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); @@ -251,7 +261,6 @@ fn process_init_reserve( msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.into()); } - // @FIXME: lending market owner is only required because of the trusted aggregator if &lending_market.owner != lending_market_owner_info.key { msg!("Lending market owner does not match the lending market owner provided"); return Err(LendingError::InvalidMarketOwner.into()); @@ -260,11 +269,13 @@ fn process_init_reserve( msg!("Lending market owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } + if &lending_market.quote_token_mint != quote_token_mint_info.key { + msg!("Lending market quote token mint does not match the quote token mint provided"); + return Err(LendingError::InvalidAccountInput.into()); + } - // @FIXME: only the lending market owner can create a reserve because there is no way to - // verify that the aggregator represents the price of any particular token let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = - if reserve_liquidity_mint_info.key == &lending_market.quote_token_mint { + if &lending_market.quote_token_mint == reserve_liquidity_mint_info.key { if account_info_iter.peek().is_some() { msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); return Err(LendingError::InvalidAccountInput.into()); @@ -273,8 +284,14 @@ fn process_init_reserve( (COption::None, 1) } else { let aggregator_info = next_account_info(account_info_iter)?; - // @FIXME: check aggregator decimals to match lending_market.quote_token_mint decimals assert_rent_exempt(rent, aggregator_info)?; + + let aggregator = Aggregator::load_initialized(aggregator_info)?; + if aggregator.config.decimals != quote_token_mint.decimals { + msg!("Quote token mint decimals does not match the aggregator config decimals provided"); + return Err(LendingError::InvalidAggregatorConfig.into()); + } + ( COption::Some(*aggregator_info.key), read_median(aggregator_info)?.median, From a9b47751561ee6cb9b32c59bee1935d97785fe93 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 16:00:29 -0500 Subject: [PATCH 168/191] remove unneeded token account checks --- token-lending/program/src/processor.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 43d7b299e8c..a3ff041bb3f 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -234,13 +234,6 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountOwner.into()); } - assert_uninitialized::(reserve_liquidity_supply_info)?; - // @TODO: why does the liquidity fee receiver need to be uninitialized? - assert_uninitialized::(reserve_liquidity_fee_receiver_info)?; - assert_uninitialized::(reserve_collateral_mint_info)?; - assert_uninitialized::(reserve_collateral_supply_info)?; - assert_uninitialized::(destination_collateral_info)?; - if reserve_liquidity_supply_info.key == source_liquidity_info.key { msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); From 54f18ba441df28c0265594c38bb261da8cc84533 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Thu, 15 Apr 2021 16:13:55 -0500 Subject: [PATCH 169/191] verify reserve liquidity fee receiver mint --- token-lending/program/src/error.rs | 13 +++--- token-lending/program/src/processor.rs | 59 ++++++++++++++++---------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index b5ba3d39dab..2c8190bbe9f 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -31,17 +31,20 @@ pub enum LendingError { /// 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, - - // 10 /// Invalid config value #[error("Input config value is invalid")] InvalidConfig, @@ -54,9 +57,6 @@ pub enum LendingError { /// Math operation overflow #[error("Math operation overflow")] MathOverflow, - /// Negative interest rate - #[error("Interest rate is negative")] - NegativeInterestRate, // 15 /// Token initialize mint failed @@ -147,6 +147,9 @@ pub enum LendingError { /// Obligation liquidity is empty #[error("Obligation liquidity is empty")] ObligationLiquidityEmpty, + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, /// Aggregator config is invalid #[error("Input aggregator config is invalid")] InvalidAggregatorConfig, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index a3ff041bb3f..6580e21998e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -239,6 +239,16 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } + let reserve_liquidity_fee_receiver = unpack_account(&reserve_liquidity_fee_receiver_info.data.borrow_mut())?; + if reserve_liquidity_fee_receiver_info.owner != token_program_id.key { + msg!("Reserve liquidity fee reserve provided is not owned by the token program provided"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if &reserve_liquidity_fee_receiver.mint != reserve_liquidity_mint_info.key { + msg!("Reserve liquidity fee reserve mint does not match the reserve liquidity mint provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + let quote_token_mint = unpack_mint("e_token_mint_info.data.borrow())?; if quote_token_mint_info.owner != token_program_id.key { msg!("Quote token mint provided is not owned by the token program provided"); @@ -1535,30 +1545,14 @@ fn assert_uninitialized( } } -/// Unpacks a spl_token `Mint`. -fn unpack_mint(data: &[u8]) -> Result { - spl_token::state::Mint::unpack(data).map_err(|_| LendingError::InvalidTokenMint) +/// Unpacks a spl_token `Account`. +fn unpack_account(data: &[u8]) -> Result { + Account::unpack(data).map_err(|_| LendingError::InvalidTokenAccount) } -/// Issue a spl_token `InitializeMint` instruction. -#[inline(always)] -fn spl_token_init_mint(params: TokenInitializeMintParams<'_, '_>) -> ProgramResult { - let TokenInitializeMintParams { - mint, - rent, - authority, - token_program, - decimals, - } = params; - let ix = spl_token::instruction::initialize_mint( - token_program.key, - mint.key, - authority, - None, - decimals, - )?; - let result = invoke(&ix, &[mint, rent, token_program]); - result.map_err(|_| LendingError::TokenInitializeMintFailed.into()) +/// Unpacks a spl_token `Mint`. +fn unpack_mint(data: &[u8]) -> Result { + Mint::unpack(data).map_err(|_| LendingError::InvalidTokenMint) } /// Issue a spl_token `InitializeAccount` instruction. @@ -1581,6 +1575,27 @@ fn spl_token_init_account(params: TokenInitializeAccountParams<'_>) -> ProgramRe result.map_err(|_| LendingError::TokenInitializeAccountFailed.into()) } +/// Issue a spl_token `InitializeMint` instruction. +#[inline(always)] +fn spl_token_init_mint(params: TokenInitializeMintParams<'_, '_>) -> ProgramResult { + let TokenInitializeMintParams { + mint, + rent, + authority, + token_program, + decimals, + } = params; + let ix = spl_token::instruction::initialize_mint( + token_program.key, + mint.key, + authority, + None, + decimals, + )?; + let result = invoke(&ix, &[mint, rent, token_program]); + result.map_err(|_| LendingError::TokenInitializeMintFailed.into()) +} + /// Issue a spl_token `Transfer` instruction. #[inline(always)] fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult { From 4471020f3aceb88b85c654ee7e275c6ee2935b3c Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 16 Apr 2021 11:27:30 -0500 Subject: [PATCH 170/191] add/remove/fix comments --- token-lending/program/src/instruction.rs | 2 +- token-lending/program/src/processor.rs | 9 ++++++--- token-lending/program/src/state/lending_market.rs | 2 -- token-lending/program/src/state/obligation.rs | 2 +- token-lending/program/src/state/reserve.rs | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 2ae411bd7ac..54c9f7b94ab 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -60,7 +60,7 @@ pub enum LendingInstruction { /// 9. `[]` Lending market account. /// 10 `[]` Derived lending market authority. /// 11 `[signer]` Lending market owner. - /// 12 `[]` User transfer authority ($authority). + /// 12 `[signer]` User transfer authority ($authority). /// 13 `[]` Clock sysvar. /// 13 `[]` Rent sysvar. /// 14 `[]` Token program id. diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6580e21998e..389e38ae3af 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -414,6 +414,7 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidAccountInput.into()); } + // @TODO: sanity check https://git.io/JOCcb reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; } else if account_info_iter.peek().is_some() { msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); @@ -713,6 +714,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ReserveStale.into()); } + // @TODO: add lookup table https://git.io/JOCYq let decimals = 10u64 .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; @@ -722,6 +724,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? .try_mul(deposit_reserve.liquidity.market_price)? .try_div(decimals)?; + // @TODO: sanity check https://git.io/JOCcb collateral.market_value = market_value; let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); @@ -761,6 +764,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ReserveStale.into()); } + // @TODO: add lookup table https://git.io/JOCYq let decimals = 10u64 .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; @@ -770,6 +774,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .borrowed_amount_wads .try_mul(borrow_reserve.liquidity.market_price)? .try_div(decimals)?; + // @TODO: sanity check https://git.io/JOCcb liquidity.market_value = market_value; borrowed_value = borrowed_value.try_add(market_value)?; } @@ -782,6 +787,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; + // @FIXME: https://git.io/JOlik if deposited_value == Decimal::zero() { obligation.loan_to_value_ratio = Rate::zero(); obligation.liquidation_threshold = Rate::zero(); @@ -1005,18 +1011,15 @@ fn process_withdraw_obligation_collateral( } let withdraw_amount = if obligation.borrows.is_empty() { - // there are no borrows; they have been repaid, liquidated, or were never taken out if collateral_amount == u64::MAX { collateral.deposited_amount } else { collateral.deposited_amount.min(collateral_amount) } } else if obligation.deposited_value == Decimal::zero() { - // there are deposits, but they cannot be valued msg!("Obligation deposited value is zero"); return Err(LendingError::ObligationDepositsZero.into()); } else { - // there are borrows and deposits, and they can both be valued let max_withdraw_value = obligation.max_withdraw_value()?; if max_withdraw_value == Decimal::zero() { msg!("Maximum withdraw value is zero"); diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/program/src/state/lending_market.rs index e6c14332240..cc821f8a2eb 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/program/src/state/lending_market.rs @@ -59,8 +59,6 @@ impl IsInitialized for LendingMarket { } } -// @TODO: Adjust padding, but what's a reasonable number? -// Or should there be no padding to save space, but we need account resizing implemented? const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128 impl Pack for LendingMarket { const LEN: usize = LENDING_MARKET_LEN; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 1f082306b4a..2ae9b94afc2 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -328,7 +328,7 @@ impl ObligationLiquidity { const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9) -// @TODO: break this up by obligation / collateral / liquidity +// @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 78bfe23b793..36d0b79e84c 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -256,7 +256,7 @@ impl Reserve { } } } else { - // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded up + // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded let liquidation_amount = obligation .max_liquidation_amount(liquidity)? .min(max_amount); @@ -689,7 +689,7 @@ impl IsInitialized for Reserve { } const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 -// @TODO: break this up by reserve / liquidity / collateral / config +// @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca impl Pack for Reserve { const LEN: usize = RESERVE_LEN; From 50ca23d561c9110b79a4d4037485e36a7523a46f Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 16 Apr 2021 11:50:03 -0500 Subject: [PATCH 171/191] update rust and remove std::u64 --- token-lending/program/src/math/rate.rs | 2 +- token-lending/program/src/processor.rs | 2 +- token-lending/program/src/state/reserve.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 5f6d2fed2c1..36674d835cc 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -23,7 +23,7 @@ use crate::{ math::{common::*, decimal::Decimal}, }; use solana_program::program_error::ProgramError; -use std::{convert::TryFrom, fmt, u64}; +use std::{convert::TryFrom, fmt}; use uint::construct_uint; // U128 with 128 bits consisting of 2 x 64-bit words diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 389e38ae3af..da2cbe8b48e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -30,7 +30,7 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::{Account, Mint}; -use std::{convert::TryFrom, u64}; +use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 36d0b79e84c..f09b1194bf2 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -16,7 +16,6 @@ use solana_program::{ use std::{ cmp::Ordering, convert::{TryFrom, TryInto}, - u64, }; /// Percentage of an obligation that can be repaid during each liquidation call From c1067fc63cdb0d0b3af47c193dc8f3e6fd04373a Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 16 Apr 2021 11:50:32 -0500 Subject: [PATCH 172/191] check market values before liquidation --- token-lending/program/src/processor.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index da2cbe8b48e..2965a216b62 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1462,8 +1462,17 @@ fn process_liquidate_obligation( let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; + if liquidity.market_value == Decimal::zero() { + msg!("Obligation borrow value is zero"); + return Err(LendingError::ObligationLiquidityEmpty.into()); + } + let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; + if collateral.market_value == Decimal::zero() { + msg!("Obligation deposit value is zero"); + return Err(LendingError::ObligationCollateralEmpty.into()); + } let authority_signer_seeds = &[ lending_market_info.key.as_ref(), From d6ef272be80dfb69ad4ec850cce89ddf40c628c2 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Fri, 16 Apr 2021 15:44:47 -0500 Subject: [PATCH 173/191] withdraw reserve doesn't need to be writable --- token-lending/program/src/instruction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 54c9f7b94ab..09c4185f83b 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -257,7 +257,7 @@ pub enum LendingInstruction { /// Minted by withdraw reserve collateral mint. /// 2. `[writable]` Repay reserve account - refreshed. /// 3. `[writable]` Repay reserve liquidity supply SPL Token account. - /// 4. `[writable]` Withdraw reserve account - refreshed. + /// 4. `[]` Withdraw reserve account - refreshed. /// 5. `[writable]` Withdraw reserve collateral supply SPL Token account. /// 6. `[writable]` Obligation account - refreshed. /// 7. `[]` Lending market account. @@ -872,7 +872,7 @@ pub fn liquidate_obligation( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new(repay_reserve_pubkey, false), AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), - AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new_readonly(withdraw_reserve_pubkey, false), AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), From 8d3ac2ec1600e9f74b09e9bc0d73859c48015b2e Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 11:11:06 -0500 Subject: [PATCH 174/191] removed genesis account test --- .../program/tests/genesis_accounts.rs | 296 ------------------ 1 file changed, 296 deletions(-) delete mode 100644 token-lending/program/tests/genesis_accounts.rs diff --git a/token-lending/program/tests/genesis_accounts.rs b/token-lending/program/tests/genesis_accounts.rs deleted file mode 100644 index 9672fe9c40e..00000000000 --- a/token-lending/program/tests/genesis_accounts.rs +++ /dev/null @@ -1,296 +0,0 @@ -// #![cfg(feature = "test-bpf")] -// -// mod helpers; -// -// use helpers::*; -// use solana_sdk::signature::Keypair; -// use spl_token_lending::{ -// instruction::AmountType, -// math::Decimal, -// state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION}, -// }; -// -// // @FIXME: test -// #[tokio::test] -// async fn test_success() { -// let (mut test, lending) = setup_test(); -// -// let LendingTest { -// sol_usdc_aggregator, -// srm_usdc_aggregator, -// usdc_mint, -// srm_mint, -// } = lending; -// -// // Initialize Lending Market -// let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); -// -// // @FIXME: dex market is gone -// // Market and collateral are setup to fill two orders in the dex market at an average -// // price of 2210.5 -// const fn lamports_to_usdc_fractional(lamports: u64) -> u64 { -// lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000 -// }; -// -// const USER_SOL_DEPOSIT_LAMPORTS: u64 = 10_000 * LAMPORTS_TO_SOL; -// const USER_SOL_COLLATERAL_LAMPORTS: u64 = 8_500 * LAMPORTS_TO_SOL; -// const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 32_500 * LAMPORTS_TO_SOL; -// const TOTAL_SOL: u64 = USER_SOL_DEPOSIT_LAMPORTS + INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS; -// const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = lamports_to_usdc_fractional(TOTAL_SOL); -// const INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL: u64 = 20_000 * FRACTIONAL_TO_SRM; -// -// let user_accounts_owner = Keypair::new(); -// -// let usdc_test_reserve = add_reserve( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddReserveArgs { -// name: "usdc".to_owned(), -// liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, -// liquidity_mint_decimals: usdc_mint.decimals, -// liquidity_mint_pubkey: usdc_mint.pubkey, -// user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, -// config: TEST_RESERVE_CONFIG, -// ..AddReserveArgs::default() -// }, -// ); -// -// let sol_test_reserve = add_reserve( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddReserveArgs { -// name: "sol".to_owned(), -// liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, -// liquidity_mint_pubkey: spl_token::native_mint::id(), -// liquidity_mint_decimals: 9, -// user_liquidity_amount: USER_SOL_DEPOSIT_LAMPORTS, -// config: TEST_RESERVE_CONFIG, -// ..AddReserveArgs::default() -// }, -// ); -// -// let srm_test_reserve = add_reserve( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddReserveArgs { -// name: "srm".to_owned(), -// aggregator_pair: Some(TestAggregatorPair::SRM_USDC), -// liquidity_amount: INITIAL_SRM_RESERVE_SUPPLY_FRACTIONAL, -// liquidity_mint_decimals: srm_mint.decimals, -// liquidity_mint_pubkey: srm_mint.pubkey, -// config: TEST_RESERVE_CONFIG, -// ..AddReserveArgs::default() -// }, -// ); -// -// let usdc_obligation = add_obligation( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddObligationArgs::default(), -// ); -// -// let sol_obligation = add_obligation( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddObligationArgs::default(), -// ); -// -// let srm_obligation = add_obligation( -// &mut test, -// &lending_market, -// &user_accounts_owner, -// AddObligationArgs::default(), -// ); -// -// let (mut banks_client, payer, _recent_blockhash) = test.start().await; -// -// // Verify lending market -// let lending_market_info = lending_market.get_state(&mut banks_client).await; -// assert_eq!(lending_market_info.version, PROGRAM_VERSION); -// assert_eq!(lending_market_info.quote_token_mint, usdc_mint.pubkey); -// -// // Verify reserves -// usdc_test_reserve.validate_state(&mut banks_client).await; -// sol_test_reserve.validate_state(&mut banks_client).await; -// srm_test_reserve.validate_state(&mut banks_client).await; -// -// let usdc_liquidity_supply = -// get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; -// assert_eq!( -// usdc_liquidity_supply, -// INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL -// ); -// let user_usdc_collateral_balance = -// get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; -// assert_eq!( -// user_usdc_collateral_balance, -// INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL -// ); -// -// let sol_liquidity_supply = -// get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; -// assert_eq!(sol_liquidity_supply, INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS); -// let user_sol_balance = -// get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; -// assert_eq!(user_sol_balance, USER_SOL_DEPOSIT_LAMPORTS); -// let user_sol_collateral_balance = -// get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; -// assert_eq!( -// user_sol_collateral_balance, -// INITIAL_COLLATERAL_RATIO * INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS -// ); -// -// // Deposit SOL -// lending_market -// .deposit( -// &mut banks_client, -// &user_accounts_owner, -// &payer, -// &sol_test_reserve, -// USER_SOL_DEPOSIT_LAMPORTS, -// ) -// .await; -// -// // Verify deposit -// let sol_liquidity_supply = -// get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; -// assert_eq!( -// sol_liquidity_supply, -// INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS + USER_SOL_DEPOSIT_LAMPORTS -// ); -// let user_sol_balance = -// get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; -// assert_eq!(user_sol_balance, 0); -// let user_sol_collateral_balance = -// get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; -// assert_eq!( -// user_sol_collateral_balance, -// INITIAL_COLLATERAL_RATIO * TOTAL_SOL -// ); -// -// // @FIXME: add deposit, refresh -// -// // Borrow USDC with SOL collateral -// lending_market -// .borrow( -// &mut banks_client, -// &payer, -// BorrowArgs { -// borrow_reserve: &usdc_test_reserve, -// // @FIXME: handle u64::MAX -// liquidity_amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, -// user_accounts_owner: &user_accounts_owner, -// obligation: &usdc_obligation, -// }, -// ) -// .await; -// -// // Borrow more USDC using existing obligation account -// lending_market -// .borrow( -// &mut banks_client, -// &payer, -// BorrowArgs { -// borrow_reserve: &usdc_test_reserve, -// // @FIXME: handle u64::MAX -// liquidity_amount: lamports_to_usdc_fractional( -// usdc_test_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS -// / 100, -// ), -// user_accounts_owner: &user_accounts_owner, -// obligation: &usdc_obligation, -// }, -// ) -// .await; -// -// // Deposit USDC -// lending_market -// .deposit( -// &mut banks_client, -// &user_accounts_owner, -// &payer, -// &usdc_test_reserve, -// // @FIXME: LTV -// 2 * INITIAL_COLLATERAL_RATIO -// * lamports_to_usdc_fractional( -// usdc_test_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS -// / 100, -// ), -// ) -// .await; -// -// // @FIXME: add deposit, refresh -// -// // Borrow SOL with USDC collateral -// lending_market -// .borrow( -// &mut banks_client, -// &payer, -// BorrowArgs { -// // @FIXME: LTV -// liquidity_amount: INITIAL_COLLATERAL_RATIO -// * lamports_to_usdc_fractional( -// usdc_test_reserve.config.loan_to_value_ratio as u64 -// * USER_SOL_COLLATERAL_LAMPORTS -// / 100, -// ), -// obligation: &sol_obligation, -// borrow_reserve: &sol_test_reserve, -// user_accounts_owner: &user_accounts_owner, -// }, -// ) -// .await; -// -// // Borrow SRM with USDC collateral -// lending_market -// .borrow( -// &mut banks_client, -// &payer, -// BorrowArgs { -// // @FIXME: LTV -// liquidity_amount: INITIAL_COLLATERAL_RATIO -// * lamports_to_usdc_fractional( -// usdc_test_reserve.config.loan_to_value_ratio as u64 -// * USER_SOL_COLLATERAL_LAMPORTS -// / 100, -// ), -// obligation: &srm_obligation, -// borrow_reserve: &srm_test_reserve, -// user_accounts_owner: &user_accounts_owner, -// }, -// ) -// .await; -// -// // Only dump the accounts if the feature is specified -// #[cfg(feature = "test-dump-genesis-accounts")] -// { -// use helpers::genesis::GenesisAccounts; -// let mut genesis_accounts = GenesisAccounts::default(); -// lending_market -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// sol_test_reserve -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// srm_test_reserve -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// usdc_test_reserve -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// sol_usdc_aggregator -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// srm_usdc_aggregator -// .add_to_genesis(&mut banks_client, &mut genesis_accounts) -// .await; -// genesis_accounts -// .insert_upgradeable_program(spl_token_lending::id(), "spl_token_lending.so"); -// genesis_accounts.write_yaml(); -// } -// } From 9f0eaa66115d387468641bcd16cebaddba1d309e Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 16:36:08 -0500 Subject: [PATCH 175/191] values are selectively checked to be nonzero where used --- token-lending/program/src/processor.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 2965a216b62..8efcdc7b399 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -724,7 +724,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? .try_mul(deposit_reserve.liquidity.market_price)? .try_div(decimals)?; - // @TODO: sanity check https://git.io/JOCcb collateral.market_value = market_value; let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); @@ -774,7 +773,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .borrowed_amount_wads .try_mul(borrow_reserve.liquidity.market_price)? .try_div(decimals)?; - // @TODO: sanity check https://git.io/JOCcb liquidity.market_value = market_value; borrowed_value = borrowed_value.try_add(market_value)?; } From b3a584eb8d6ba13fb06f780c9c4847c0ef9cefd1 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 17:04:31 -0500 Subject: [PATCH 176/191] simplify obligation LTV and liquidation threshold --- token-lending/program/src/processor.rs | 29 +++++-------- token-lending/program/src/state/obligation.rs | 43 +++++++++---------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 8efcdc7b399..e756119ea47 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -685,8 +685,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut deposited_value = Decimal::zero(); let mut borrowed_value = Decimal::zero(); - let mut loan_to_value_ratio = Decimal::zero(); - let mut liquidation_threshold = Decimal::zero(); + let mut allowed_borrow_value = Decimal::zero(); + let mut unhealthy_borrow_value = Decimal::zero(); for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; @@ -731,10 +731,10 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Rate::from_percent(deposit_reserve.config.liquidation_threshold); deposited_value = deposited_value.try_add(market_value)?; - loan_to_value_ratio = - loan_to_value_ratio.try_add(market_value.try_mul(loan_to_value_rate)?)?; - liquidation_threshold = - liquidation_threshold.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; + allowed_borrow_value = + allowed_borrow_value.try_add(market_value.try_mul(loan_to_value_rate)?)?; + unhealthy_borrow_value = + unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { @@ -784,17 +784,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; - - // @FIXME: https://git.io/JOlik - if deposited_value == Decimal::zero() { - obligation.loan_to_value_ratio = Rate::zero(); - obligation.liquidation_threshold = Rate::zero(); - } else { - obligation.loan_to_value_ratio = - Rate::try_from(loan_to_value_ratio.try_div(deposited_value)?)?; - obligation.liquidation_threshold = - Rate::try_from(liquidation_threshold.try_div(deposited_value)?)?; - } + obligation.allowed_borrow_value = allowed_borrow_value; + obligation.unhealthy_borrow_value = unhealthy_borrow_value; obligation.last_update.update_slot(clock.slot); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1169,7 +1160,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let remaining_borrow_value = obligation.max_borrow_value()?; + let remaining_borrow_value = obligation.remaining_borrow_value()?; if remaining_borrow_value == Decimal::zero() { msg!("Remaining borrow value is zero"); return Err(LendingError::BorrowTooLarge.into()); @@ -1453,7 +1444,7 @@ fn process_liquidate_obligation( msg!("Obligation borrowed value is zero"); return Err(LendingError::ObligationBorrowsZero.into()); } - if obligation.loan_to_value()? < Decimal::from(obligation.liquidation_threshold) { + if obligation.borrowed_value < obligation.unhealthy_borrow_value { msg!("Obligation is healthy and cannot be liquidated"); return Err(LendingError::ObligationHealthy.into()); } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 2ae9b94afc2..8476f7f96a1 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -39,10 +39,10 @@ pub struct Obligation { pub deposited_value: Decimal, /// Market value of borrows pub borrowed_value: Decimal, - /// The target ratio of borrowed value to deposited value - pub loan_to_value_ratio: Rate, - /// The loan to value ratio at which the obligation can be liquidated - pub liquidation_threshold: Rate, + /// The maximum borrow value at the weighted average loan to value ratio + pub allowed_borrow_value: Decimal, + /// The dangerous borrow value at the weighted average liquidation threshold + pub unhealthy_borrow_value: Decimal, } impl Obligation { @@ -90,22 +90,19 @@ impl Obligation { Ok(()) } - /// Calculate the maximum collateral value that can be withdrawn for a given loan to value ratio + /// Calculate the maximum collateral value that can be withdrawn pub fn max_withdraw_value(&self) -> Result { - let min_deposited_value = self.borrowed_value.try_div(self.loan_to_value_ratio)?; - if min_deposited_value >= self.deposited_value { + let loan_to_value_ratio = self.allowed_borrow_value.try_div(self.deposited_value)?; + let required_deposit_value = self.borrowed_value.try_div(loan_to_value_ratio)?; + if required_deposit_value >= self.deposited_value { return Ok(Decimal::zero()); } - self.deposited_value.try_sub(min_deposited_value) + self.deposited_value.try_sub(required_deposit_value) } - /// Calculate the maximum liquidity value that can be borrowed for a given loan to value ratio - pub fn max_borrow_value(&self) -> Result { - let max_borrowed_value = self.deposited_value.try_mul(self.loan_to_value_ratio)?; - if self.borrowed_value >= max_borrowed_value { - return Ok(Decimal::zero()); - } - max_borrowed_value.try_sub(self.borrowed_value) + /// 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) } /// Calculate the maximum liquidation amount for a given liquidity @@ -343,8 +340,8 @@ impl Pack for Obligation { owner, deposited_value, borrowed_value, - loan_to_value_ratio, - liquidation_threshold, + allowed_borrow_value, + unhealthy_borrow_value, deposits_len, borrows_len, data_flat, @@ -372,8 +369,8 @@ impl Pack for Obligation { owner.copy_from_slice(self.owner.as_ref()); pack_decimal(self.deposited_value, deposited_value); pack_decimal(self.borrowed_value, borrowed_value); - pack_rate(self.loan_to_value_ratio, loan_to_value_ratio); - pack_rate(self.liquidation_threshold, liquidation_threshold); + pack_decimal(self.allowed_borrow_value, allowed_borrow_value); + pack_decimal(self.unhealthy_borrow_value, 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(); @@ -420,8 +417,8 @@ impl Pack for Obligation { owner, deposited_value, borrowed_value, - loan_to_value_ratio, - liquidation_threshold, + allowed_borrow_value, + unhealthy_borrow_value, deposits_len, borrows_len, data_flat, @@ -491,8 +488,8 @@ impl Pack for Obligation { borrows, deposited_value: unpack_decimal(deposited_value), borrowed_value: unpack_decimal(borrowed_value), - loan_to_value_ratio: unpack_rate(loan_to_value_ratio), - liquidation_threshold: unpack_rate(liquidation_threshold), + allowed_borrow_value: unpack_decimal(allowed_borrow_value), + unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), }) } } From 187a21a2fabed9d05ea33c8bdc653bb5187d0882 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 17:06:33 -0500 Subject: [PATCH 177/191] simplify algebra: borrowed_value/(allowed_borrow_value/deposited_value) = (borrowed_value*deposited_value)/allowed_borrow_value --- token-lending/program/src/state/obligation.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 8476f7f96a1..5776bf9e957 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -92,8 +92,9 @@ impl Obligation { /// Calculate the maximum collateral value that can be withdrawn pub fn max_withdraw_value(&self) -> Result { - let loan_to_value_ratio = self.allowed_borrow_value.try_div(self.deposited_value)?; - let required_deposit_value = self.borrowed_value.try_div(loan_to_value_ratio)?; + let required_deposit_value = self.borrowed_value + .try_mul(self.deposited_value)? + .try_div(self.allowed_borrow_value)?; if required_deposit_value >= self.deposited_value { return Ok(Decimal::zero()); } From 68c123d1eb460d3312bb1113f2a0183ac4f9b5d7 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 17:17:27 -0500 Subject: [PATCH 178/191] fix fmt / clippy --- token-lending/program/src/processor.rs | 60 ++++++++++--------- token-lending/program/src/state/mod.rs | 10 +--- token-lending/program/src/state/obligation.rs | 5 +- token-lending/program/src/state/reserve.rs | 4 +- 4 files changed, 37 insertions(+), 42 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e756119ea47..439749f93ee 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -11,11 +11,7 @@ use crate::{ ReserveCollateral, ReserveConfig, ReserveLiquidity, }, }; -use flux_aggregator::{ - borsh_state::InitBorshState, - read_median, - state::Aggregator -}; +use flux_aggregator::{borsh_state::InitBorshState, read_median, state::Aggregator}; use num_traits::FromPrimitive; use solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -30,7 +26,6 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::{Account, Mint}; -use std::convert::TryFrom; /// Processes an instruction pub fn process_instruction( @@ -239,13 +234,16 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } - let reserve_liquidity_fee_receiver = unpack_account(&reserve_liquidity_fee_receiver_info.data.borrow_mut())?; + let reserve_liquidity_fee_receiver = + unpack_account(&reserve_liquidity_fee_receiver_info.data.borrow_mut())?; if reserve_liquidity_fee_receiver_info.owner != token_program_id.key { msg!("Reserve liquidity fee reserve provided is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } if &reserve_liquidity_fee_receiver.mint != reserve_liquidity_mint_info.key { - msg!("Reserve liquidity fee reserve mint does not match the reserve liquidity mint provided"); + msg!( + "Reserve liquidity fee reserve mint does not match the reserve liquidity mint provided" + ); return Err(LendingError::InvalidAccountInput.into()); } @@ -277,29 +275,33 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } - let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = - if &lending_market.quote_token_mint == reserve_liquidity_mint_info.key { - if account_info_iter.peek().is_some() { - msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); - return Err(LendingError::InvalidAccountInput.into()); - } - // 1 because quote token price is equal to itself - (COption::None, 1) - } else { - let aggregator_info = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, aggregator_info)?; + let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = if &lending_market + .quote_token_mint + == reserve_liquidity_mint_info.key + { + if account_info_iter.peek().is_some() { + msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); + return Err(LendingError::InvalidAccountInput.into()); + } + // 1 because quote token price is equal to itself + (COption::None, 1) + } else { + let aggregator_info = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, aggregator_info)?; - let aggregator = Aggregator::load_initialized(aggregator_info)?; - if aggregator.config.decimals != quote_token_mint.decimals { - msg!("Quote token mint decimals does not match the aggregator config decimals provided"); - return Err(LendingError::InvalidAggregatorConfig.into()); - } + let aggregator = Aggregator::load_initialized(aggregator_info)?; + if aggregator.config.decimals != quote_token_mint.decimals { + msg!( + "Quote token mint decimals does not match the aggregator config decimals provided" + ); + return Err(LendingError::InvalidAggregatorConfig.into()); + } - ( - COption::Some(*aggregator_info.key), - read_median(aggregator_info)?.median, - ) - }; + ( + COption::Some(*aggregator_info.key), + read_median(aggregator_info)?.median, + ) + }; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), diff --git a/token-lending/program/src/state/mod.rs b/token-lending/program/src/state/mod.rs index b604e238a29..38529af00b1 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/program/src/state/mod.rs @@ -10,7 +10,7 @@ pub use lending_market::*; pub use obligation::*; pub use reserve::*; -use crate::math::{Decimal, Rate, U128, WAD}; +use crate::math::{Decimal, WAD}; use arrayref::{array_refs, mut_array_refs}; use solana_program::{ clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, @@ -75,14 +75,6 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal { Decimal::from_scaled_val(u128::from_le_bytes(*src)) } -fn pack_rate(rate: Rate, dst: &mut [u8; 16]) { - *dst = rate.to_scaled_val().to_le_bytes(); -} - -fn unpack_rate(src: &[u8; 16]) -> Rate { - Rate(U128::from(u128::from_le_bytes(*src))) -} - fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { *dst = (boolean as u8).to_le_bytes() } diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 5776bf9e957..c3cc6a91552 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -92,7 +92,8 @@ impl Obligation { /// Calculate the maximum collateral value that can be withdrawn pub fn max_withdraw_value(&self) -> Result { - let required_deposit_value = self.borrowed_value + let required_deposit_value = self + .borrowed_value .try_mul(self.deposited_value)? .try_div(self.allowed_borrow_value)?; if required_deposit_value >= self.deposited_value { @@ -326,7 +327,7 @@ impl ObligationLiquidity { const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16 const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16 const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9) -// @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca + // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca impl Pack for Obligation { const LEN: usize = OBLIGATION_LEN; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f09b1194bf2..f9d2f3ec0de 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -688,7 +688,7 @@ impl IsInitialized for Reserve { } const RESERVE_LEN: usize = 567; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + (4 + 32) + 16 + 8 + 8 + 16 + 32 + 32 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 1 + 256 -// @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca + // @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -796,7 +796,7 @@ impl Pack for Reserve { fn unpack_from_slice(input: &[u8]) -> Result { let input = array_ref![input, 0, RESERVE_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let ( + let ( version, last_update_slot, last_update_stale, From 289eb566c75381967b324bb6ff092030b8f957b3 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 17:55:41 -0500 Subject: [PATCH 179/191] use oracle instead of aggregator except where using an instance of Aggregator --- token-lending/program/src/error.rs | 6 ++-- token-lending/program/src/instruction.rs | 16 +++++----- token-lending/program/src/processor.rs | 32 +++++++++---------- token-lending/program/src/state/reserve.rs | 18 +++++------ token-lending/program/tests/helpers/mod.rs | 29 +++++++++-------- .../program/tests/obligation_end_to_end.rs | 2 +- .../program/tests/refresh_obligation.rs | 2 +- .../program/tests/refresh_reserve.rs | 2 +- 8 files changed, 54 insertions(+), 53 deletions(-) diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 2c8190bbe9f..969d9f59b87 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -150,9 +150,9 @@ pub enum LendingError { /// Negative interest rate #[error("Interest rate is negative")] NegativeInterestRate, - /// Aggregator config is invalid - #[error("Input aggregator config is invalid")] - InvalidAggregatorConfig, + /// Oracle config is invalid + #[error("Input oracle config is invalid")] + InvalidOracleConfig, } impl From for ProgramError { diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 09c4185f83b..bc0d47d8ff5 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -64,7 +64,7 @@ pub enum LendingInstruction { /// 13 `[]` Clock sysvar. /// 13 `[]` Rent sysvar. /// 14 `[]` Token program id. - /// 15 `[optional]` Reserve liquidity aggregator account. + /// 15 `[optional]` Reserve liquidity oracle account. /// Not required for quote currency reserves. /// Must match base and quote currency mint, and quote currency decimals. InitReserve { @@ -81,7 +81,7 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Reserve account. /// 1. `[]` Clock sysvar. - /// 2. `[optional]` Reserve liquidity aggregator account. + /// 2. `[optional]` Reserve liquidity oracle account. /// Required if the reserve currency is not the lending market quote /// currency. RefreshReserve, @@ -531,7 +531,7 @@ pub fn init_reserve( lending_market_pubkey: Pubkey, lending_market_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, - reserve_liquidity_aggregator_pubkey: Option, + reserve_liquidity_oracle_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], @@ -555,9 +555,9 @@ pub fn init_reserve( AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; - if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + if let Some(reserve_liquidity_oracle_pubkey) = reserve_liquidity_oracle_pubkey { accounts.push(AccountMeta::new_readonly( - reserve_liquidity_aggregator_pubkey, + reserve_liquidity_oracle_pubkey, false, )); } @@ -576,15 +576,15 @@ pub fn init_reserve( pub fn refresh_reserve( program_id: Pubkey, reserve_pubkey: Pubkey, - reserve_liquidity_aggregator_pubkey: Option, + reserve_liquidity_oracle_pubkey: Option, ) -> Instruction { let mut accounts = vec![ AccountMeta::new(reserve_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; - if let Some(reserve_liquidity_aggregator_pubkey) = reserve_liquidity_aggregator_pubkey { + if let Some(reserve_liquidity_oracle_pubkey) = reserve_liquidity_oracle_pubkey { accounts.push(AccountMeta::new_readonly( - reserve_liquidity_aggregator_pubkey, + reserve_liquidity_oracle_pubkey, false, )); } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 439749f93ee..3f6ac659725 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -275,31 +275,31 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } - let (reserve_liquidity_aggregator, reserve_liquidity_market_price) = if &lending_market + let (reserve_liquidity_oracle_pubkey, reserve_liquidity_market_price) = if &lending_market .quote_token_mint == reserve_liquidity_mint_info.key { if account_info_iter.peek().is_some() { - msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); + msg!("Reserve liquidity oracle cannot be provided when reserve liquidity is the quote currency"); return Err(LendingError::InvalidAccountInput.into()); } // 1 because quote token price is equal to itself (COption::None, 1) } else { - let aggregator_info = next_account_info(account_info_iter)?; - assert_rent_exempt(rent, aggregator_info)?; + let reserve_liquidity_oracle_info = next_account_info(account_info_iter)?; + assert_rent_exempt(rent, reserve_liquidity_oracle_info)?; - let aggregator = Aggregator::load_initialized(aggregator_info)?; + let aggregator = Aggregator::load_initialized(reserve_liquidity_oracle_info)?; if aggregator.config.decimals != quote_token_mint.decimals { msg!( "Quote token mint decimals does not match the aggregator config decimals provided" ); - return Err(LendingError::InvalidAggregatorConfig.into()); + return Err(LendingError::InvalidOracleConfig.into()); } ( - COption::Some(*aggregator_info.key), - read_median(aggregator_info)?.median, + COption::Some(*reserve_liquidity_oracle_info.key), + read_median(reserve_liquidity_oracle_info)?.median, ) }; @@ -330,7 +330,7 @@ fn process_init_reserve( mint_decimals: reserve_liquidity_mint.decimals, supply_pubkey: *reserve_liquidity_supply_info.key, fee_receiver: *reserve_liquidity_fee_receiver_info.key, - aggregator: reserve_liquidity_aggregator, + oracle_pubkey: reserve_liquidity_oracle_pubkey, market_price: reserve_liquidity_market_price, }), collateral: ReserveCollateral::new(NewReserveCollateralParams { @@ -407,19 +407,17 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro return Err(LendingError::InvalidAccountOwner.into()); } - if let COption::Some(reserve_liquidity_aggregator) = reserve.liquidity.aggregator { - let reserve_liquidity_aggregator_info = next_account_info(account_info_iter)?; - if &reserve_liquidity_aggregator != reserve_liquidity_aggregator_info.key { - msg!( - "Reserve liquidity aggregator does not match the reserve liquidity aggregator provided" - ); + if let COption::Some(reserve_liquidity_oracle_pubkey) = reserve.liquidity.oracle_pubkey { + let reserve_liquidity_oracle_info = next_account_info(account_info_iter)?; + if &reserve_liquidity_oracle_pubkey != reserve_liquidity_oracle_info.key { + msg!("Reserve liquidity oracle does not match the reserve liquidity oracle provided"); return Err(LendingError::InvalidAccountInput.into()); } // @TODO: sanity check https://git.io/JOCcb - reserve.liquidity.market_price = read_median(reserve_liquidity_aggregator_info)?.median; + reserve.liquidity.market_price = read_median(reserve_liquidity_oracle_info)?.median; } else if account_info_iter.peek().is_some() { - msg!("Reserve liquidity aggregator cannot be provided when reserve liquidity is the quote currency"); + msg!("Reserve liquidity oracle cannot be provided when reserve liquidity is the quote currency"); return Err(LendingError::InvalidAccountInput.into()); } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f9d2f3ec0de..58b56e7e456 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -355,8 +355,8 @@ pub struct ReserveLiquidity { pub supply_pubkey: Pubkey, /// Reserve liquidity fee receiver address pub fee_receiver: Pubkey, - /// Optional reserve liquidity aggregator state account - pub aggregator: COption, + /// Optional reserve liquidity oracle state account + pub oracle_pubkey: COption, /// Reserve liquidity available pub available_amount: u64, /// Reserve liquidity borrowed @@ -375,7 +375,7 @@ impl ReserveLiquidity { mint_decimals: params.mint_decimals, supply_pubkey: params.supply_pubkey, fee_receiver: params.fee_receiver, - aggregator: params.aggregator, + oracle_pubkey: params.oracle_pubkey, cumulative_borrow_rate_wads: Decimal::one(), market_price: params.market_price, available_amount: 0, @@ -476,8 +476,8 @@ pub struct NewReserveLiquidityParams { pub supply_pubkey: Pubkey, /// Reserve liquidity fee receiver address pub fee_receiver: Pubkey, - /// Optional reserve liquidity aggregator state account - pub aggregator: COption, + /// Optional reserve liquidity oracle state account + pub oracle_pubkey: COption, /// Reserve liquidity market price in quote currency pub market_price: u64, } @@ -704,7 +704,7 @@ impl Pack for Reserve { liquidity_mint_decimals, liquidity_supply_pubkey, liquidity_fee_receiver, - liquidity_aggregator, + liquidity_oracle_pubkey, liquidity_available_amount, liquidity_borrowed_amount_wads, liquidity_cumulative_borrow_rate_wads, @@ -763,7 +763,7 @@ impl Pack for Reserve { *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); liquidity_supply_pubkey.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref()); - pack_coption_key(&self.liquidity.aggregator, liquidity_aggregator); + pack_coption_key(&self.liquidity.oracle_pubkey, liquidity_oracle_pubkey); *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); pack_decimal( self.liquidity.borrowed_amount_wads, @@ -805,7 +805,7 @@ impl Pack for Reserve { liquidity_mint_decimals, liquidity_supply_pubkey, liquidity_fee_receiver, - liquidity_aggregator, + liquidity_oracle_pubkey, liquidity_available_amount, liquidity_borrowed_amount_wads, liquidity_cumulative_borrow_rate_wads, @@ -871,7 +871,7 @@ impl Pack for Reserve { mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey), fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver), - aggregator: unpack_coption_key(liquidity_aggregator)?, + oracle_pubkey: unpack_coption_key(liquidity_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), diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 32efdd5b1a7..562d9f79b95 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -281,8 +281,7 @@ pub fn add_reserve( aggregator_pair, } = args; - let (liquidity_aggregator_pubkey, market_price) = if let Some(aggregator_pair) = aggregator_pair - { + let (liquidity_oracle_pubkey, market_price) = if let Some(aggregator_pair) = aggregator_pair { let aggregator = add_aggregator(test, aggregator_pair); (Some(aggregator.pubkey), aggregator.price) } else if liquidity_mint_pubkey == spl_token::native_mint::id() { @@ -389,7 +388,7 @@ pub fn add_reserve( mint_decimals: liquidity_mint_decimals, supply_pubkey: liquidity_supply_pubkey, fee_receiver: liquidity_fee_receiver_pubkey, - aggregator: liquidity_aggregator_pubkey.into(), + oracle_pubkey: liquidity_oracle_pubkey.into(), market_price, }), collateral: ReserveCollateral::new(NewReserveCollateralParams { @@ -468,7 +467,7 @@ pub fn add_reserve( collateral_supply_pubkey, user_liquidity_pubkey, user_collateral_pubkey, - liquidity_aggregator_pubkey, + liquidity_oracle_pubkey, market_price, } } @@ -552,7 +551,7 @@ impl TestLendingMarket { &[refresh_reserve( spl_token_lending::id(), reserve.pubkey, - reserve.liquidity_aggregator_pubkey, + reserve.liquidity_oracle_pubkey, )], Some(&payer.pubkey()), ); @@ -732,11 +731,11 @@ pub struct TestReserve { pub liquidity_supply_pubkey: Pubkey, pub liquidity_fee_receiver_pubkey: Pubkey, pub liquidity_host_pubkey: Pubkey, + pub liquidity_oracle_pubkey: Option, pub collateral_mint_pubkey: Pubkey, pub collateral_supply_pubkey: Pubkey, pub user_liquidity_pubkey: Pubkey, pub user_collateral_pubkey: Pubkey, - pub liquidity_aggregator_pubkey: Option, pub market_price: u64, } @@ -764,7 +763,7 @@ impl TestReserve { let user_collateral_token_keypair = Keypair::new(); let user_transfer_authority_keypair = Keypair::new(); - let (liquidity_aggregator_pubkey, market_price) = if let Some(aggregator) = aggregator { + let (liquidity_oracle_pubkey, market_price) = if let Some(aggregator) = aggregator { (Some(aggregator.pubkey), aggregator.price) } else if liquidity_mint_pubkey == lending_market.quote_token_mint { (None, 1 * FRACTIONAL_TO_USDC) @@ -852,10 +851,11 @@ impl TestReserve { liquidity_fee_receiver_keypair.pubkey(), collateral_mint_keypair.pubkey(), collateral_supply_keypair.pubkey(), + lending_market.quote_token_mint, lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority_keypair.pubkey(), - liquidity_aggregator_pubkey, + liquidity_oracle_pubkey, ), ], Some(&payer.pubkey()), @@ -896,7 +896,7 @@ impl TestReserve { collateral_supply_pubkey: collateral_supply_keypair.pubkey(), user_liquidity_pubkey, user_collateral_pubkey: user_collateral_token_keypair.pubkey(), - liquidity_aggregator_pubkey, + liquidity_oracle_pubkey, market_price, }) .map_err(|e| e.unwrap()) @@ -990,14 +990,17 @@ impl TestReserve { ); assert_eq!(self.config, reserve.config); - let liquidity_aggregator_coption = - if let Some(liquidity_aggregator_pubkey) = self.liquidity_aggregator_pubkey { - COption::Some(liquidity_aggregator_pubkey) + let liquidity_oracle_coption = + if let Some(liquidity_oracle_pubkey) = self.liquidity_oracle_pubkey { + COption::Some(liquidity_oracle_pubkey) } else { COption::None }; - assert_eq!(liquidity_aggregator_coption, reserve.liquidity.aggregator); + assert_eq!( + liquidity_oracle_coption, + reserve.liquidity.oracle_pubkey + ); assert_eq!( reserve.liquidity.cumulative_borrow_rate_wads, Decimal::one() diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index a7963a50334..09077ede708 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -128,7 +128,7 @@ async fn test_success() { refresh_reserve( spl_token_lending::id(), sol_test_reserve.pubkey, - sol_test_reserve.liquidity_aggregator_pubkey, + sol_test_reserve.liquidity_oracle_pubkey, ), // 3 approve( diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 307df7c23cb..e8f6518b2da 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -107,7 +107,7 @@ async fn test_success() { refresh_reserve( spl_token_lending::id(), sol_test_reserve.pubkey, - sol_test_reserve.liquidity_aggregator_pubkey, + sol_test_reserve.liquidity_oracle_pubkey, ), refresh_obligation( spl_token_lending::id(), diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs index c59fe29c54e..a0ce320b61e 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -90,7 +90,7 @@ async fn test_success() { refresh_reserve( spl_token_lending::id(), sol_test_reserve.pubkey, - sol_test_reserve.liquidity_aggregator_pubkey, + sol_test_reserve.liquidity_oracle_pubkey, ), ], Some(&payer.pubkey()), From e355f36a4493c7321f5d38e3050f169a1eccd594 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 18:23:32 -0500 Subject: [PATCH 180/191] initialize reserve liquidity fee receiver --- token-lending/program/src/processor.rs | 28 +++++++++----------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 3f6ac659725..f1b751b6537 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -25,7 +25,7 @@ use solana_program::{ pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; -use spl_token::state::{Account, Mint}; +use spl_token::state::Mint; /// Processes an instruction pub fn process_instruction( @@ -234,19 +234,6 @@ fn process_init_reserve( return Err(LendingError::InvalidAccountInput.into()); } - let reserve_liquidity_fee_receiver = - unpack_account(&reserve_liquidity_fee_receiver_info.data.borrow_mut())?; - if reserve_liquidity_fee_receiver_info.owner != token_program_id.key { - msg!("Reserve liquidity fee reserve provided is not owned by the token program provided"); - return Err(LendingError::InvalidTokenOwner.into()); - } - if &reserve_liquidity_fee_receiver.mint != reserve_liquidity_mint_info.key { - msg!( - "Reserve liquidity fee reserve mint does not match the reserve liquidity mint provided" - ); - return Err(LendingError::InvalidAccountInput.into()); - } - let quote_token_mint = unpack_mint("e_token_mint_info.data.borrow())?; if quote_token_mint_info.owner != token_program_id.key { msg!("Quote token mint provided is not owned by the token program provided"); @@ -351,6 +338,14 @@ fn process_init_reserve( token_program: token_program_id.clone(), })?; + spl_token_init_account(TokenInitializeAccountParams { + account: reserve_liquidity_fee_receiver_info.clone(), + mint: reserve_liquidity_mint_info.clone(), + owner: lending_market_authority_info.clone(), + rent: rent_info.clone(), + token_program: token_program_id.clone(), + })?; + spl_token_init_mint(TokenInitializeMintParams { mint: reserve_collateral_mint_info.clone(), authority: lending_market_authority_info.key, @@ -1546,11 +1541,6 @@ fn assert_uninitialized( } } -/// Unpacks a spl_token `Account`. -fn unpack_account(data: &[u8]) -> Result { - Account::unpack(data).map_err(|_| LendingError::InvalidTokenAccount) -} - /// Unpacks a spl_token `Mint`. fn unpack_mint(data: &[u8]) -> Result { Mint::unpack(data).map_err(|_| LendingError::InvalidTokenMint) From 32323a8ff93450bcb839a2922fa93cad73761d31 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 20:38:44 -0500 Subject: [PATCH 181/191] receive amount is missing the fee --- token-lending/program/src/processor.rs | 4 +--- token-lending/program/src/state/reserve.rs | 9 +++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index f1b751b6537..fffce244376 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1173,9 +1173,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::BorrowTooSmall.into()); } - borrow_reserve - .liquidity - .borrow(borrow_amount, receive_amount)?; + borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 58b56e7e456..e3e00525d28 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -411,17 +411,18 @@ impl ReserveLiquidity { } /// Subtract borrow amount from available liquidity and add to borrows - pub fn borrow(&mut self, borrow_amount: Decimal, receive_amount: u64) -> ProgramResult { - if receive_amount > self.available_amount { + 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(receive_amount) + .checked_sub(borrow_amount) .ok_or(LendingError::MathOverflow)?; - self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_decimal)?; Ok(()) } From acfc83afc36baff57336782554e439609457b80f Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 20:38:59 -0500 Subject: [PATCH 182/191] add todo comment --- token-lending/program/src/state/reserve.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e3e00525d28..068ef0d58a1 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -142,6 +142,7 @@ impl Reserve { amount_to_borrow: u64, max_borrow_value: 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)?; From ddde4688c895806d64ebcaaece5bac17a0b738af Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 20:39:12 -0500 Subject: [PATCH 183/191] fix variable name --- token-lending/program/src/state/reserve.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 068ef0d58a1..e88ea4425dd 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -151,19 +151,19 @@ impl Reserve { .try_mul(decimals)? .try_div(self.liquidity.market_price)? .min(self.liquidity.available_amount.into()); - let (origination_fee, host_fee) = self + 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(origination_fee) + .checked_sub(borrow_fee) .ok_or(LendingError::MathOverflow)?; Ok(CalculateBorrowResult { borrow_amount, receive_amount, - borrow_fee: origination_fee, + borrow_fee, host_fee, }) } else { From ffde502e146540337a3e8ff11debbb4dcae87bf1 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 20:39:57 -0500 Subject: [PATCH 184/191] fix tests --- .../tests/borrow_obligation_liquidity.rs | 2 - .../tests/deposit_obligation_collateral.rs | 9 +-- token-lending/program/tests/helpers/mod.rs | 24 +++----- .../program/tests/init_lending_market.rs | 2 +- token-lending/program/tests/init_reserve.rs | 3 +- .../program/tests/liquidate_obligation.rs | 6 +- .../program/tests/obligation_end_to_end.rs | 21 ++----- .../tests/repay_obligation_liquidity.rs | 2 - .../tests/withdraw_obligation_collateral.rs | 56 +++---------------- 9 files changed, 26 insertions(+), 99 deletions(-) diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index ae6c49b0370..f8a93b5b9fa 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -115,7 +115,6 @@ async fn test_borrow_quote_currency() { transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); assert!(banks_client.process_transaction(transaction).await.is_ok()); - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; let obligation = test_obligation.get_state(&mut banks_client).await; @@ -261,7 +260,6 @@ async fn test_borrow_max_base_currency() { assert!(banks_client.process_transaction(transaction).await.is_ok()); let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; let obligation = test_obligation.get_state(&mut banks_client).await; let (total_fee, host_fee) = sol_reserve diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index 338c4546773..cba1709fcbd 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -7,14 +7,9 @@ use solana_program_test::*; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, - system_instruction::create_account, transaction::Transaction, }; -use spl_token::{ - instruction::approve, - solana_program::program_pack::Pack, - state::{Account as Token, Mint}, -}; +use spl_token::instruction::approve; use spl_token_lending::{ instruction::deposit_obligation_collateral, processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, @@ -71,8 +66,6 @@ async fn test_success() { let initial_user_collateral_balance = get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let rent = banks_client.get_rent().await.unwrap(); - let mut transaction = Transaction::new_with_payer( &[ approve( diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 562d9f79b95..9d2a0232cf5 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -24,10 +24,8 @@ use spl_token::{ }; use spl_token_lending::{ instruction::{ - borrow_obligation_liquidity, deposit_obligation_collateral, deposit_reserve_liquidity, - init_lending_market, init_obligation, init_reserve, liquidate_obligation, - redeem_reserve_collateral, refresh_obligation, refresh_reserve, repay_obligation_liquidity, - set_lending_market_owner, withdraw_obligation_collateral, + borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market, + init_obligation, init_reserve, liquidate_obligation, refresh_reserve, }, math::{Decimal, Rate, TryAdd, TryMul}, processor::process_instruction, @@ -253,7 +251,6 @@ pub struct AddReserveArgs { pub borrow_amount: u64, pub initial_borrow_rate: u8, pub collateral_amount: u64, - pub fees_amount: u64, pub mark_fresh: bool, pub slots_elapsed: u64, pub aggregator_pair: Option, @@ -275,7 +272,6 @@ pub fn add_reserve( borrow_amount, initial_borrow_rate, collateral_amount, - fees_amount, mark_fresh, slots_elapsed, aggregator_pair, @@ -357,7 +353,7 @@ pub fn add_reserve( &Token { mint: liquidity_mint_pubkey, owner: lending_market.owner.pubkey(), - amount: fees_amount, + amount: 0, state: AccountState::Initialized, ..Token::default() }, @@ -371,7 +367,7 @@ pub fn add_reserve( &Token { mint: liquidity_mint_pubkey, owner: user_accounts_owner.pubkey(), - amount: fees_amount, + amount: 0, state: AccountState::Initialized, ..Token::default() }, @@ -398,10 +394,7 @@ pub fn add_reserve( config, }); reserve.deposit_liquidity(liquidity_amount).unwrap(); - reserve - .liquidity - .borrow(borrow_amount.into(), borrow_amount) - .unwrap(); + reserve.liquidity.borrow(borrow_amount.into()).unwrap(); let borrow_rate_multiplier = Rate::one() .try_add(Rate::from_percent(initial_borrow_rate)) .unwrap(); @@ -997,10 +990,7 @@ impl TestReserve { COption::None }; - assert_eq!( - liquidity_oracle_coption, - reserve.liquidity.oracle_pubkey - ); + assert_eq!(liquidity_oracle_coption, reserve.liquidity.oracle_pubkey); assert_eq!( reserve.liquidity.cumulative_borrow_rate_wads, Decimal::one() @@ -1202,7 +1192,9 @@ pub struct TestAggregator { pub fn add_aggregator(test: &mut ProgramTest, pair: TestAggregatorPair) -> TestAggregator { let (name, decimals, price) = match pair { + // price @ 1 SOL = 20 USDC TestAggregatorPair::SOL_USDC => ("SOL:USDC", 6, 20 * FRACTIONAL_TO_USDC), + // price @ 1 SRM = 5 USDC TestAggregatorPair::SRM_USDC => ("SRM:USDC", 6, 5 * FRACTIONAL_TO_USDC), }; diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index ef6c2aa4860..92cb8fae711 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -23,7 +23,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(8_000); + test.set_bpf_compute_max_units(10_000); let usdc_mint = add_usdc_mint(&mut test); let (mut banks_client, payer, _recent_blockhash) = test.start().await; diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 7bba2ab110d..23b335a4b27 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -26,7 +26,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(62_000); + test.set_bpf_compute_max_units(66_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); @@ -119,6 +119,7 @@ async fn test_already_initialized() { usdc_test_reserve.liquidity_fee_receiver_pubkey, usdc_test_reserve.collateral_mint_pubkey, usdc_test_reserve.collateral_supply_pubkey, + lending_market.quote_token_mint, lending_market.pubkey, lending_market.owner.pubkey(), user_transfer_authority.pubkey(), diff --git a/token-lending/program/tests/liquidate_obligation.rs b/token-lending/program/tests/liquidate_obligation.rs index 77441a76d13..d24e1edd88e 100644 --- a/token-lending/program/tests/liquidate_obligation.rs +++ b/token-lending/program/tests/liquidate_obligation.rs @@ -10,9 +10,9 @@ use solana_sdk::{ transaction::Transaction, }; use spl_token::instruction::approve; -use spl_token_lending::instruction::{liquidate_obligation, refresh_obligation}; use spl_token_lending::{ - instruction::repay_obligation_liquidity, processor::process_instruction, + instruction::{liquidate_obligation, refresh_obligation}, + processor::process_instruction, state::INITIAL_COLLATERAL_RATIO, }; @@ -140,8 +140,6 @@ async fn test_success() { ); assert!(banks_client.process_transaction(transaction).await.is_ok()); - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let user_liquidity_balance = get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; assert_eq!( diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index 09077ede708..20189dc7f72 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -6,17 +6,12 @@ use helpers::*; use solana_program_test::*; use solana_sdk::{ account::Account, - instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction::create_account, transaction::Transaction, }; -use spl_token::{ - instruction::approve, - solana_program::program_pack::Pack, - state::{Account as Token, Mint}, -}; +use spl_token::{instruction::approve, solana_program::program_pack::Pack}; use spl_token_lending::{ instruction::{ borrow_obligation_liquidity, deposit_obligation_collateral, init_obligation, @@ -45,10 +40,9 @@ async fn test_success() { const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC - FEE_AMOUNT; - const USDC_REPAY_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT_FRACTIONAL + FEE_AMOUNT; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = - USDC_BORROW_AMOUNT_FRACTIONAL + USDC_REPAY_AMOUNT_FRACTIONAL; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL - FEE_AMOUNT; + const USDC_REPAY_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL; let user_accounts_owner = Keypair::new(); let user_accounts_owner_pubkey = user_accounts_owner.pubkey(); @@ -84,7 +78,7 @@ async fn test_success() { &lending_market, &user_accounts_owner, AddReserveArgs { - user_liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + user_liquidity_amount: FEE_AMOUNT, liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, liquidity_mint_pubkey: usdc_mint.pubkey, liquidity_mint_decimals: usdc_mint.decimals, @@ -234,7 +228,6 @@ async fn test_success() { ); assert!(banks_client.process_transaction(transaction).await.is_ok()); - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; let obligation = { @@ -263,10 +256,6 @@ async fn test_success() { initial_user_liquidity_balance - FEE_AMOUNT ); assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, Decimal::zero()); - // @FIXME: - // thread 'test_success' panicked at 'assertion failed: `(left == right)` - // left: `2000000000`, - // right: `1999999900`', token-lending/program/tests/obligation_end_to_end.rs:266:5 assert_eq!( usdc_reserve.liquidity.available_amount, initial_liquidity_supply diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs index 7a8f7292984..fb0ce93304a 100644 --- a/token-lending/program/tests/repay_obligation_liquidity.rs +++ b/token-lending/program/tests/repay_obligation_liquidity.rs @@ -124,8 +124,6 @@ async fn test_success() { ); assert!(banks_client.process_transaction(transaction).await.is_ok()); - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let user_liquidity_balance = get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; assert_eq!( diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 6c7ea63f5ea..0ab688c8555 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -10,7 +10,6 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, }; -use spl_token::instruction::approve; use spl_token_lending::{ error::LendingError, instruction::{refresh_obligation, withdraw_obligation_collateral}, @@ -36,7 +35,6 @@ async fn test_withdraw_base_currency_fixed_amount() { const WITHDRAW_AMOUNT: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -99,15 +97,6 @@ async fn test_withdraw_base_currency_fixed_amount() { let mut transaction = Transaction::new_with_payer( &[ - approve( - &spl_token::id(), - &sol_test_reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -121,16 +110,13 @@ async fn test_withdraw_base_currency_fixed_amount() { sol_test_reserve.pubkey, test_obligation.pubkey, lending_market.pubkey, - user_transfer_authority.pubkey(), + test_obligation.owner, ), ], Some(&payer.pubkey()), ); - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); assert!(banks_client.process_transaction(transaction).await.is_ok()); // check that collateral tokens were transferred @@ -172,7 +158,6 @@ async fn test_withdraw_quote_currency_all() { const WITHDRAW_AMOUNT: u64 = u64::MAX; let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -220,15 +205,6 @@ async fn test_withdraw_quote_currency_all() { let mut transaction = Transaction::new_with_payer( &[ - approve( - &spl_token::id(), - &usdc_test_reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -242,16 +218,13 @@ async fn test_withdraw_quote_currency_all() { usdc_test_reserve.pubkey, test_obligation.pubkey, lending_market.pubkey, - user_transfer_authority.pubkey(), + test_obligation.owner, ), ], Some(&payer.pubkey()), ); - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); assert!(banks_client.process_transaction(transaction).await.is_ok()); // check that collateral tokens were transferred @@ -289,7 +262,6 @@ async fn test_withdraw_too_large() { const WITHDRAW_AMOUNT: u64 = (100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO) + 1; let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); @@ -336,21 +308,10 @@ async fn test_withdraw_too_large() { }, ); - let test_collateral = &test_obligation.deposits[0]; - let (mut banks_client, payer, recent_blockhash) = test.start().await; let mut transaction = Transaction::new_with_payer( &[ - approve( - &spl_token::id(), - &sol_test_reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - WITHDRAW_AMOUNT, - ) - .unwrap(), refresh_obligation( spl_token_lending::id(), test_obligation.pubkey, @@ -364,16 +325,13 @@ async fn test_withdraw_too_large() { sol_test_reserve.pubkey, test_obligation.pubkey, lending_market.pubkey, - user_transfer_authority.pubkey(), + test_obligation.owner, ), ], Some(&payer.pubkey()), ); - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); // check that transaction fails assert_eq!( @@ -383,7 +341,7 @@ async fn test_withdraw_too_large() { .unwrap_err() .unwrap(), TransactionError::InstructionError( - 3, + 1, InstructionError::Custom(LendingError::WithdrawTooLarge as u32) ) ); From 896e0eefb520b77dafdf183b9fc48ca0312441e7 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 21:07:12 -0500 Subject: [PATCH 185/191] reverted mint_total_supply change --- token-lending/program/src/state/reserve.rs | 26 +++++++++++----------- token-lending/program/tests/helpers/mod.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index e88ea4425dd..a332a13ab1b 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -490,7 +490,7 @@ pub struct ReserveCollateral { /// Reserve collateral mint address pub mint_pubkey: Pubkey, /// Reserve collateral mint supply, used for exchange rate - pub mint_amount: u64, + pub mint_total_supply: u64, /// Reserve collateral supply address pub supply_pubkey: Pubkey, } @@ -500,15 +500,15 @@ impl ReserveCollateral { pub fn new(params: NewReserveCollateralParams) -> Self { Self { mint_pubkey: params.mint_pubkey, - mint_amount: 0, + 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_amount = self - .mint_amount + self.mint_total_supply = self + .mint_total_supply .checked_add(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -516,8 +516,8 @@ impl ReserveCollateral { /// Remove collateral from total supply pub fn burn(&mut self, collateral_amount: u64) -> ProgramResult { - self.mint_amount = self - .mint_amount + self.mint_total_supply = self + .mint_total_supply .checked_sub(collateral_amount) .ok_or(LendingError::MathOverflow)?; Ok(()) @@ -528,11 +528,11 @@ impl ReserveCollateral { &self, total_liquidity: Decimal, ) -> Result { - let rate = if self.mint_amount == 0 || total_liquidity == Decimal::zero() { + let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() { Rate::from_scaled_val(INITIAL_COLLATERAL_RATE) } else { - let mint_amount = Decimal::from(self.mint_amount); - Rate::try_from(mint_amount.try_div(total_liquidity)?)? + let mint_total_supply = Decimal::from(self.mint_total_supply); + Rate::try_from(mint_total_supply.try_div(total_liquidity)?)? }; Ok(CollateralExchangeRate(rate)) @@ -712,7 +712,7 @@ impl Pack for Reserve { liquidity_cumulative_borrow_rate_wads, liquidity_market_price, collateral_mint_pubkey, - collateral_mint_amount, + collateral_mint_total_supply, collateral_supply_pubkey, config_optimal_utilization_rate, config_loan_to_value_ratio, @@ -779,7 +779,7 @@ impl Pack for Reserve { // collateral collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref()); - *collateral_mint_amount = self.collateral.mint_amount.to_le_bytes(); + *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 @@ -813,7 +813,7 @@ impl Pack for Reserve { liquidity_cumulative_borrow_rate_wads, liquidity_market_price, collateral_mint_pubkey, - collateral_mint_amount, + collateral_mint_total_supply, collateral_supply_pubkey, config_optimal_utilization_rate, config_loan_to_value_ratio, @@ -881,7 +881,7 @@ impl Pack for Reserve { }, collateral: ReserveCollateral { mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), - mint_amount: u64::from_le_bytes(*collateral_mint_amount), + mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply), supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey), }, config: ReserveConfig { diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 9d2a0232cf5..7af4c4dc7b9 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -997,7 +997,7 @@ impl TestReserve { ); assert_eq!(reserve.liquidity.borrowed_amount_wads, Decimal::zero()); assert!(reserve.liquidity.available_amount > 0); - assert!(reserve.collateral.mint_amount > 0); + assert!(reserve.collateral.mint_total_supply > 0); } } From 52cafdef778d61764785f0817e51eba2daef4d6c Mon Sep 17 00:00:00 2001 From: jordansexton Date: Tue, 27 Apr 2021 21:33:19 -0500 Subject: [PATCH 186/191] remove token-lending/client, clippy passing --- Cargo.lock | 184 ++++++++----------- Cargo.toml | 1 - token-lending/client/Cargo.toml | 15 -- token-lending/client/src/main.rs | 304 ------------------------------- 4 files changed, 76 insertions(+), 428 deletions(-) delete mode 100644 token-lending/client/Cargo.toml delete mode 100644 token-lending/client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 96091b3e188..c7714d7550a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,12 +40,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-traits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483" - [[package]] name = "ansi_term" version = "0.11.0" @@ -244,29 +238,61 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "borsh" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b13fa9bf62be34702e5ee4526aff22530ae22fe34a0c4290d30d5e4e782e6" +dependencies = [ + "borsh-derive 0.7.2", +] + [[package]] name = "borsh" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" dependencies = [ - "borsh-derive", + "borsh-derive 0.8.2", "hashbrown", ] +[[package]] +name = "borsh-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6aaa45f8eec26e4bf71e7e5492cf53a91591af8f871f422d550e7cc43f6b927" +dependencies = [ + "borsh-derive-internal 0.7.2", + "borsh-schema-derive-internal 0.7.2", + "proc-macro2 1.0.24", + "syn 1.0.64", +] + [[package]] name = "borsh-derive" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", + "borsh-derive-internal 0.8.2", + "borsh-schema-derive-internal 0.8.2", "proc-macro-crate", "proc-macro2 1.0.24", "syn 1.0.64", ] +[[package]] +name = "borsh-derive-internal" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61621b9d3cca65cc54e2583db84ef912d59ae60d2f04ba61bc0d7fc57556bda2" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.64", +] + [[package]] name = "borsh-derive-internal" version = "0.8.2" @@ -278,6 +304,17 @@ dependencies = [ "syn 1.0.64", ] +[[package]] +name = "borsh-schema-derive-internal" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b38abfda570837b0949c2c7ebd31417e15607861c23eacb2f668c69f6f3bf7" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.64", +] + [[package]] name = "borsh-schema-derive-internal" version = "0.8.2" @@ -323,12 +360,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a4bad0c5981acc24bc09e532f35160f952e35422603f0563cd7a73c2c2e65a0" - [[package]] name = "byteorder" version = "1.4.2" @@ -919,26 +950,6 @@ dependencies = [ "syn 1.0.64", ] -[[package]] -name = "enumflags2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" -dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.8", - "syn 1.0.64", -] - [[package]] name = "env_logger" version = "0.8.3" @@ -964,16 +975,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" -[[package]] -name = "field-offset" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40e7a744c1d22cd64783732a287dd5d08a9f0e1d89b685bf084aab753cb20d4" -dependencies = [ - "memoffset 0.5.6", - "rustc_version", -] - [[package]] name = "filetime" version = "0.2.13" @@ -998,6 +999,22 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flux-aggregator" +version = "0.1.0" +source = "git+https://github.com/octopus-network/solana-flux-aggregator?rev=9cfaec5#9cfaec5a2977751c6cb8ceac8ee6cffe3a1b64c0" +dependencies = [ + "borsh 0.7.2", + "borsh-derive 0.7.2", + "byteorder", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "spl-token 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2667,12 +2684,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "safe-transmute" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b8b2cd387f744f69469aaed197954ba4c0ecdb31e02edf99b023e0df11178a" - [[package]] name = "same-file" version = "1.0.6" @@ -2848,29 +2859,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "serum_dex" -version = "0.2.0" -source = "git+https://github.com/project-serum/serum-dex?rev=991a86e#991a86e93c22667b6b0fbb01914395d332be5531" -dependencies = [ - "arrayref", - "bincode", - "bytemuck", - "byteorder", - "enumflags2", - "field-offset", - "itertools", - "num-traits", - "num_enum", - "safe-transmute", - "serde", - "solana-program", - "spl-token 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "static_assertions", - "thiserror", - "without-alloc", -] - [[package]] name = "sha-1" version = "0.8.2" @@ -3015,8 +3003,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e398e020121174a8c7d0817ecd03bea68d0540d1c35f0b1ae1c192c496b8d3" dependencies = [ "bincode", - "borsh", - "borsh-derive", + "borsh 0.8.2", + "borsh-derive 0.8.2", "futures 0.3.12", "mio 0.7.7", "solana-banks-interface", @@ -3331,8 +3319,8 @@ checksum = "885552ce43e9f2cf13fda274bf2b4ef75c5de6e5e0190f53acb83f84cda739c0" dependencies = [ "bincode", "blake3", - "borsh", - "borsh-derive", + "borsh 0.8.2", + "borsh-derive 0.8.2", "bs58 0.3.1", "bv", "curve25519-dalek 2.1.2", @@ -3669,7 +3657,7 @@ name = "spl-binary-oracle-pair" version = "0.1.0" dependencies = [ "arbitrary", - "borsh", + "borsh 0.8.2", "num-derive", "num-traits", "solana-program", @@ -3729,8 +3717,8 @@ dependencies = [ name = "spl-feature-proposal" version = "1.0.0" dependencies = [ - "borsh", - "borsh-derive", + "borsh 0.8.2", + "borsh-derive 0.8.2", "futures 0.3.12", "solana-program", "solana-program-test", @@ -3756,8 +3744,8 @@ dependencies = [ name = "spl-math" version = "0.1.0" dependencies = [ - "borsh", - "borsh-derive", + "borsh 0.8.2", + "borsh-derive 0.8.2", "num-derive", "num-traits", "proptest", @@ -3790,8 +3778,8 @@ dependencies = [ name = "spl-record" version = "0.1.0" dependencies = [ - "borsh", - "borsh-derive", + "borsh 0.8.2", + "borsh-derive 0.8.2", "num-derive", "num-traits", "solana-program", @@ -3816,7 +3804,7 @@ version = "0.1.0" dependencies = [ "arrayref", "bincode", - "borsh", + "borsh 0.8.2", "num-derive", "num-traits", "num_enum", @@ -3837,7 +3825,7 @@ name = "spl-stake-pool-cli" version = "2.0.1" dependencies = [ "bincode", - "borsh", + "borsh 0.8.2", "bs58 0.4.0", "clap", "lazy_static", @@ -3906,13 +3894,13 @@ dependencies = [ "arrayref", "assert_matches", "base64 0.13.0", + "flux-aggregator", "log", "num-derive", "num-traits", "proptest", "serde", "serde_yaml", - "serum_dex", "solana-program", "solana-program-test", "solana-sdk", @@ -3921,17 +3909,6 @@ dependencies = [ "uint", ] -[[package]] -name = "spl-token-lending-client" -version = "0.1.0" -dependencies = [ - "solana-client", - "solana-program", - "solana-sdk", - "spl-token 3.1.0", - "spl-token-lending", -] - [[package]] name = "spl-token-swap" version = "2.1.0" @@ -4866,15 +4843,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "without-alloc" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e34736feff52a0b3e5680927e947a4d8fac1f0b80dc8120b080dd8de24d75e2" -dependencies = [ - "alloc-traits", -] - [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 705b8dc735d..0cf6c31a6cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "stake-pool/cli", "stake-pool/program", "token-lending/program", - "token-lending/client", "token-swap/program", "token-swap/program/fuzz", "token/cli", diff --git a/token-lending/client/Cargo.toml b/token-lending/client/Cargo.toml deleted file mode 100644 index b5cb11a0b56..00000000000 --- a/token-lending/client/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "spl-token-lending-client" -version = "0.1.0" -description = "Solana Program Library Token Lending Client" -authors = ["Solana Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2018" - -[dependencies] -solana-client = "1.6.6" -solana-program = "1.6.6" -solana-sdk = "1.6.6" -spl-token-lending = { path = "../program", features = [ "no-entrypoint" ] } -spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] } diff --git a/token-lending/client/src/main.rs b/token-lending/client/src/main.rs deleted file mode 100644 index 0585b9d8cc8..00000000000 --- a/token-lending/client/src/main.rs +++ /dev/null @@ -1,304 +0,0 @@ -use solana_client::rpc_client::RpcClient; -use solana_program::program_pack::Pack; -use solana_sdk::{ - pubkey::Pubkey, - signature::{read_keypair_file, Keypair, Signer}, - system_instruction::create_account, - transaction::Transaction, -}; -use spl_token::{ - instruction::approve, - state::{Account as Token, Mint}, -}; -use spl_token_lending::{ - instruction::{init_lending_market, init_reserve}, - state::{LendingMarket, Reserve, ReserveConfig, ReserveFees}, -}; -use std::str::FromStr; - -// -------- UPDATE START ------- -const KEYPAIR_PATH: &str = "/your/path"; -const SRM_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS"; -const USDC_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS"; -const WRAPPED_SOL_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS"; -solana_program::declare_id!("TokenLend1ng1111111111111111111111111111111"); -// -------- UPDATE END --------- - -pub struct DexMarket { - pub name: &'static str, - pub pubkey: Pubkey, -} - -pub fn main() { - let mut client = RpcClient::new("https://api.mainnet-beta.solana.com".to_owned()); - - let payer = read_keypair_file(&format!("{}/payer.json", KEYPAIR_PATH)).unwrap(); - let usdc_mint_pubkey = - Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); - - let sol_usdc_dex_market = DexMarket { - name: "sol_usdc", - pubkey: Pubkey::from_str("9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT").unwrap(), - }; - - let srm_usdc_dex_market = DexMarket { - name: "srm_usdc", - pubkey: Pubkey::from_str("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA").unwrap(), - }; - - let quote_token_mint = usdc_mint_pubkey; - let (lending_market_owner, lending_market_pubkey, _lending_market) = - create_lending_market(&mut client, quote_token_mint, &payer); - - let usdc_liquidity_source = Pubkey::from_str(USDC_TOKEN_ACCOUNT).unwrap(); - let usdc_reserve_config = ReserveConfig { - optimal_utilization_rate: 80, - loan_to_value_ratio: 75, - liquidation_bonus: 5, - liquidation_threshold: 80, - min_borrow_rate: 0, - optimal_borrow_rate: 4, - max_borrow_rate: 30, - fees: ReserveFees { - borrow_fee_wad: 100_000_000_000_000, // 1 bp - host_fee_percentage: 20, - }, - }; - - let (usdc_reserve_pubkey, _usdc_reserve) = create_reserve( - &mut client, - usdc_reserve_config, - lending_market_pubkey, - &lending_market_owner, - None, - usdc_liquidity_source, - &payer, - ); - - println!("Created usdc reserve with pubkey: {}", usdc_reserve_pubkey); - - let sol_liquidity_source = Pubkey::from_str(WRAPPED_SOL_TOKEN_ACCOUNT).unwrap(); - let sol_reserve_config = ReserveConfig { - optimal_utilization_rate: 0, - loan_to_value_ratio: 75, - liquidation_bonus: 10, - liquidation_threshold: 80, - min_borrow_rate: 0, - optimal_borrow_rate: 2, - max_borrow_rate: 15, - fees: ReserveFees { - borrow_fee_wad: 1_000_000_000_000, // 0.01 bp - host_fee_percentage: 20, - }, - }; - - let (sol_reserve_pubkey, _sol_reserve) = create_reserve( - &mut client, - sol_reserve_config, - lending_market_pubkey, - &lending_market_owner, - Some(sol_usdc_dex_market.pubkey), - sol_liquidity_source, - &payer, - ); - - println!("Created sol reserve with pubkey: {}", sol_reserve_pubkey); - - let srm_liquidity_source = Pubkey::from_str(SRM_TOKEN_ACCOUNT).unwrap(); - let srm_reserve_config = ReserveConfig { - optimal_utilization_rate: 0, - loan_to_value_ratio: 75, - liquidation_bonus: 10, - liquidation_threshold: 80, - min_borrow_rate: 0, - optimal_borrow_rate: 2, - max_borrow_rate: 15, - fees: ReserveFees { - borrow_fee_wad: 10_000_000_000_000, // 0.1 bp - host_fee_percentage: 25, - }, - }; - - let (srm_reserve_pubkey, _srm_reserve) = create_reserve( - &mut client, - srm_reserve_config, - lending_market_pubkey, - &lending_market_owner, - Some(srm_usdc_dex_market.pubkey), - srm_liquidity_source, - &payer, - ); - - println!("Created srm reserve with pubkey: {}", srm_reserve_pubkey); -} - -pub fn create_lending_market( - client: &mut RpcClient, - quote_token_mint: Pubkey, - payer: &Keypair, -) -> (Keypair, Pubkey, LendingMarket) { - let owner = read_keypair_file(&format!("{}/lending_market_owner.json", KEYPAIR_PATH)).unwrap(); - let keypair = Keypair::new(); - let pubkey = keypair.pubkey(); - - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &pubkey, - client - .get_minimum_balance_for_rent_exemption(LendingMarket::LEN) - .unwrap(), - LendingMarket::LEN as u64, - &id(), - ), - init_lending_market(id(), pubkey, owner.pubkey(), quote_token_mint), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = client.get_recent_blockhash().unwrap().0; - transaction.sign(&[&payer, &keypair], recent_blockhash); - client.send_and_confirm_transaction(&transaction).unwrap(); - - let account = client.get_account(&pubkey).unwrap(); - let lending_market = LendingMarket::unpack(&account.data).unwrap(); - - (owner, pubkey, lending_market) -} - -pub fn create_reserve( - client: &mut RpcClient, - config: ReserveConfig, - lending_market_pubkey: Pubkey, - lending_market_owner: &Keypair, - dex_market_pubkey: Option, - liquidity_source_pubkey: Pubkey, - payer: &Keypair, -) -> (Pubkey, Reserve) { - let reserve_keypair = Keypair::new(); - let reserve_pubkey = reserve_keypair.pubkey(); - let collateral_mint_keypair = Keypair::new(); - let collateral_supply_keypair = Keypair::new(); - let liquidity_supply_keypair = Keypair::new(); - let liquidity_fee_receiver_keypair = Keypair::new(); - let user_collateral_token_keypair = Keypair::new(); - let user_transfer_authority = Keypair::new(); - - let liquidity_source_account = client.get_account(&liquidity_source_pubkey).unwrap(); - let liquidity_source_token = Token::unpack(&liquidity_source_account.data).unwrap(); - let liquidity_mint_pubkey = liquidity_source_token.mint; - - let recent_blockhash = client.get_recent_blockhash().unwrap().0; - let token_balance = client - .get_minimum_balance_for_rent_exemption(Token::LEN) - .unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &collateral_mint_keypair.pubkey(), - client - .get_minimum_balance_for_rent_exemption(Mint::LEN) - .unwrap(), - Mint::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &collateral_supply_keypair.pubkey(), - token_balance, - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &liquidity_supply_keypair.pubkey(), - token_balance, - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &liquidity_fee_receiver_keypair.pubkey(), - token_balance, - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &user_collateral_token_keypair.pubkey(), - token_balance, - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &reserve_pubkey, - client - .get_minimum_balance_for_rent_exemption(Reserve::LEN) - .unwrap(), - Reserve::LEN as u64, - &id(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &vec![ - payer, - &reserve_keypair, - &collateral_mint_keypair, - &collateral_supply_keypair, - &liquidity_supply_keypair, - &user_collateral_token_keypair, - ], - recent_blockhash, - ); - - client.send_and_confirm_transaction(&transaction).unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &liquidity_source_pubkey, - &user_transfer_authority.pubkey(), - &payer.pubkey(), - &[], - liquidity_source_token.amount, - ) - .unwrap(), - init_reserve( - id(), - liquidity_source_token.amount, - config, - liquidity_source_pubkey, - user_collateral_token_keypair.pubkey(), - reserve_pubkey, - liquidity_mint_pubkey, - liquidity_supply_keypair.pubkey(), - collateral_mint_keypair.pubkey(), - collateral_supply_keypair.pubkey(), - liquidity_fee_receiver_keypair.pubkey(), - lending_market_pubkey, - lending_market_owner.pubkey(), - user_transfer_authority.pubkey(), - dex_market_pubkey, - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &vec![payer, &lending_market_owner, &user_transfer_authority], - recent_blockhash, - ); - - client.send_and_confirm_transaction(&transaction).unwrap(); - - let account = client.get_account(&reserve_pubkey).unwrap(); - (reserve_pubkey, Reserve::unpack(&account.data).unwrap()) -} From 522eba4f6cfdf23fe69421a97847d84b6ef7d19f Mon Sep 17 00:00:00 2001 From: jordansexton Date: Wed, 28 Apr 2021 12:28:17 -0500 Subject: [PATCH 187/191] fix js test --- token-lending/js/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/js/client/index.ts b/token-lending/js/client/index.ts index 6737139e2ba..5fe02b5b24e 100644 --- a/token-lending/js/client/index.ts +++ b/token-lending/js/client/index.ts @@ -30,7 +30,7 @@ export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.s Layout.publicKey("owner"), Layout.publicKey("quoteTokenMint"), Layout.publicKey("tokenProgramId"), - BufferLayout.blob(62, "padding"), + BufferLayout.blob(128, "padding"), ] ); From 1f4b96df9eb82423706fa8ac0bcc104131739e9d Mon Sep 17 00:00:00 2001 From: jordansexton Date: Wed, 28 Apr 2021 12:36:21 -0500 Subject: [PATCH 188/191] restore unit tests from master --- token-lending/program/src/state/obligation.rs | 142 ++++ token-lending/program/src/state/reserve.rs | 608 ++++++++++++++++++ 2 files changed, 750 insertions(+) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index c3cc6a91552..ce5e41b1e96 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -495,3 +495,145 @@ impl Pack for Obligation { }) } } + +// @FIXME: test +#[cfg(test)] +mod test { + use super::*; + use crate::math::TryAdd; + use proptest::prelude::*; + + const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000% + + #[test] + fn obligation_accrue_interest_failure() { + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::zero(), + ..Obligation::default() + } + .accrue_interest(Decimal::one()), + Err(LendingError::MathOverflow.into()) + ); + + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::from(2u64), + ..Obligation::default() + } + .accrue_interest(Decimal::one()), + Err(LendingError::NegativeInterestRate.into()) + ); + + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_liquidity_wads: Decimal::from(u64::MAX), + ..Obligation::default() + } + .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), + Err(LendingError::MathOverflow.into()) + ); + } + + // Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1 + prop_compose! { + fn cumulative_rates()(rate in 1..=u128::MAX)( + current_rate in Just(rate), + max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128) + ) -> (u128, u128) { + (current_rate, max_new_rate) + } + } + + const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128; + + // Creates liquidity amounts (repay, borrow) where repay < borrow + prop_compose! { + fn repay_partial_amounts()(repay in 1..=u64::MAX)( + liquidity_amount in Just(repay), + borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED + ) -> (u64, u128) { + (liquidity_amount, borrowed_liquidity) + } + } + + // Creates liquidity amounts (repay, borrow) where repay >= borrow + prop_compose! { + fn repay_full_amounts()(repay in 1..=u64::MAX)( + liquidity_amount in Just(repay), + borrowed_liquidity in 0..=(WAD as u128 * repay as u128) + ) -> (u64, u128) { + (liquidity_amount, borrowed_liquidity) + } + } + + // Creates collateral amounts (collateral, obligation tokens) where c <= ot + prop_compose! { + fn collateral_amounts()(collateral in 1..=u64::MAX)( + deposited_collateral_tokens in Just(collateral), + obligation_tokens in collateral..=u64::MAX + ) -> (u64, u64) { + (deposited_collateral_tokens, obligation_tokens) + } + } + + proptest! { + #[test] + fn repay_partial( + (liquidity_amount, borrowed_liquidity) in repay_partial_amounts(), + (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), + ) { + let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); + let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() }; + + let repay_result = state.repay(liquidity_amount, obligation_tokens)?; + assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); + assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens); + assert!(repay_result.obligation_token_amount < obligation_tokens); + assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads); + assert!(state.borrowed_liquidity_wads > Decimal::zero()); + assert!(state.deposited_collateral_tokens > 0); + + let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?; + let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?; + assert!(obligation_token_rate <= collateral_withdraw_rate); + } + + #[test] + fn repay_full( + (liquidity_amount, borrowed_liquidity) in repay_full_amounts(), + (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), + ) { + let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); + let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() } ; + + let repay_result = state.repay(liquidity_amount, obligation_tokens)?; + assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); + assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens); + assert_eq!(repay_result.obligation_token_amount, obligation_tokens); + assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads); + assert_eq!(state.borrowed_liquidity_wads, Decimal::zero()); + assert_eq!(state.deposited_collateral_tokens, 0); + } + + #[test] + fn accrue_interest( + borrowed_liquidity in 0..=u64::MAX, + (current_borrow_rate, new_borrow_rate) in cumulative_rates(), + ) { + let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity); + let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; + let mut state = Obligation { cumulative_borrow_rate_wads, borrowed_liquidity_wads, ..Obligation::default() }; + + let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; + state.accrue_interest(next_cumulative_borrow_rate)?; + + if next_cumulative_borrow_rate > cumulative_borrow_rate_wads { + assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); + } else { + assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); + } + } + } +} diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index a332a13ab1b..a0657a129ab 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -900,3 +900,611 @@ impl Pack for Reserve { }) } } + +// @FIXME: unit tests +#[cfg(test)] +mod test { + use super::*; + use crate::math::{PERCENT_SCALER, WAD}; + use proptest::prelude::*; + use std::cmp::Ordering; + + const MAX_LIQUIDITY: u64 = u64::MAX / 5; + + struct MockConverter(Decimal); + impl TokenConverter for MockConverter { + fn best_price(&mut self, _token_mint: &Pubkey) -> Result { + Ok(self.0) + } + + fn convert( + self, + from_amount: Decimal, + _from_token_mint: &Pubkey, + ) -> Result { + from_amount.try_mul(self.0) + } + } + + /// Convert reserve liquidity tokens to the collateral tokens of another reserve + fn liquidity_in_other_collateral( + liquidity_amount: u64, + collateral_exchange_rate: CollateralExchangeRate, + conversion_rate: Decimal, + ) -> Result { + collateral_exchange_rate.decimal_liquidity_to_collateral( + Decimal::from(liquidity_amount).try_mul(conversion_rate)?, + ) + } + + // Creates rates (min, opt, max) where 0 <= min <= opt <= max <= MAX + prop_compose! { + fn borrow_rates()(optimal_rate in 0..=u8::MAX)( + min_rate in 0..=optimal_rate, + optimal_rate in Just(optimal_rate), + max_rate in optimal_rate..=u8::MAX, + ) -> (u8, u8, u8) { + (min_rate, optimal_rate, max_rate) + } + } + + // Creates rates (threshold, ltv) where 2 <= threshold <= 100 and threshold <= ltv <= 1,000% + prop_compose! { + fn unhealthy_rates()(threshold in 2..=100u8)( + ltv_rate in threshold as u64..=1000u64, + threshold in Just(threshold), + ) -> (Decimal, u8) { + (Decimal::from_scaled_val(ltv_rate as u128 * PERCENT_SCALER as u128), threshold) + } + } + + // Creates a range of reasonable token conversion rates + prop_compose! { + fn token_conversion_rate()( + conversion_rate in 1..=u16::MAX, + invert_conversion_rate: bool, + ) -> Decimal { + let conversion_rate = Decimal::from(conversion_rate as u64); + if invert_conversion_rate { + Decimal::one().try_div(conversion_rate).unwrap() + } else { + conversion_rate + } + } + } + + // Creates a range of reasonable collateral exchange rates + prop_compose! { + fn collateral_exchange_rate_range()(percent in 1..=500u64) -> CollateralExchangeRate { + CollateralExchangeRate(Rate::from_scaled_val(percent * PERCENT_SCALER)) + } + } + + proptest! { + #[test] + fn unhealthy_obligations_can_be_liquidated( + obligation_collateral in 1..=u64::MAX, + (obligation_ltv, liquidation_threshold) in unhealthy_rates(), + collateral_exchange_rate in collateral_exchange_rate_range(), + token_conversion_rate in token_conversion_rate(), + ) { + let collateral_reserve_config = &ReserveConfig { + liquidation_threshold, + ..ReserveConfig::default() + }; + + // Create unhealthy obligation at target LTV + let collateral_value = collateral_exchange_rate + .decimal_collateral_to_liquidity(Decimal::from(obligation_collateral as u64))? + .try_div(token_conversion_rate)?; + + // Ensure that collateral value fits in u64 + prop_assume!(collateral_value.try_round_u64().is_ok()); + + let borrowed_liquidity_wads = collateral_value + .try_mul(obligation_ltv)? + .try_add(Decimal::from_scaled_val(1u128))? // ensure loan is unhealthy + .max(Decimal::from(2u64)); // avoid dust account closure + + // Ensure that borrow value fits in u64 + prop_assume!(borrowed_liquidity_wads.try_round_u64().is_ok()); + + let obligation = Obligation { + deposited_collateral_tokens: obligation_collateral as u64, + borrowed_liquidity_wads, + ..Obligation::default() + }; + + // Ensure that the token conversion fits in a Decimal + { + let token_converter = MockConverter(token_conversion_rate); + let decimal_repay_amount = Decimal::from(obligation.max_liquidation_amount()?); + // Calculate the amount of collateral that will be received + let receive_liquidity_amount_result = + token_converter.convert(decimal_repay_amount, &Pubkey::default()); + prop_assume!(receive_liquidity_amount_result.is_ok()); + } + + // Liquidate with max amount to ensure obligation can be liquidated + let liquidate_result = Reserve::_liquidate_obligation( + &obligation, + u64::MAX, + &Pubkey::default(), + collateral_exchange_rate, + collateral_reserve_config, + MockConverter(token_conversion_rate) + ); + + let liquidate_result = liquidate_result.unwrap(); + let expected_withdraw_amount = liquidity_in_other_collateral( + liquidate_result.repay_amount, + collateral_exchange_rate, + token_conversion_rate, + )?.min(obligation.deposited_collateral_tokens.into()); + + assert!(liquidate_result.repay_amount > 0); + assert!(liquidate_result.withdraw_amount > 0); + + let min_withdraw_amount = expected_withdraw_amount.try_floor_u64()?; + let max_withdraw_amount = expected_withdraw_amount.try_ceil_u64()?; + let max_repay_amount = obligation.borrowed_liquidity_wads + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? + .try_ceil_u64()?; + + assert!(liquidate_result.withdraw_amount >= min_withdraw_amount); + assert!(liquidate_result.withdraw_amount <= max_withdraw_amount); + assert!(liquidate_result.repay_amount <= max_repay_amount); + + let defaulted = liquidate_result.withdraw_amount == obligation.deposited_collateral_tokens; + if defaulted { + assert_eq!(liquidate_result.settle_amount, borrowed_liquidity_wads); + assert!(liquidate_result.repay_amount < liquidate_result.settle_amount.try_floor_u64()?); + } else { + assert_eq!(liquidate_result.settle_amount.try_ceil_u64()?, liquidate_result.repay_amount); + assert!(liquidate_result.withdraw_amount < obligation.deposited_collateral_tokens); + } + } + + #[test] + fn current_borrow_rate( + total_liquidity in 0..=MAX_LIQUIDITY, + borrowed_percent in 0..=WAD, + optimal_utilization_rate in 0..=100u8, + (min_borrow_rate, optimal_borrow_rate, max_borrow_rate) in borrow_rates(), + ) { + let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; + let reserve = Reserve { + liquidity: ReserveLiquidity { + borrowed_amount_wads, + available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?, + ..ReserveLiquidity::default() + }, + config: ReserveConfig { optimal_utilization_rate, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, ..ReserveConfig::default() }, + ..Reserve::default() + }; + + let current_borrow_rate = reserve.current_borrow_rate()?; + assert!(current_borrow_rate >= Rate::from_percent(min_borrow_rate)); + assert!(current_borrow_rate <= Rate::from_percent(max_borrow_rate)); + + let optimal_borrow_rate = Rate::from_percent(optimal_borrow_rate); + let current_rate = reserve.liquidity.utilization_rate()?; + match current_rate.cmp(&Rate::from_percent(optimal_utilization_rate)) { + Ordering::Less => { + if min_borrow_rate == reserve.config.optimal_borrow_rate { + assert_eq!(current_borrow_rate, optimal_borrow_rate); + } else { + assert!(current_borrow_rate < optimal_borrow_rate); + } + } + Ordering::Equal => assert!(current_borrow_rate == optimal_borrow_rate), + Ordering::Greater => { + if max_borrow_rate == reserve.config.optimal_borrow_rate { + assert_eq!(current_borrow_rate, optimal_borrow_rate); + } else { + assert!(current_borrow_rate > optimal_borrow_rate); + } + } + } + } + + #[test] + fn allowed_borrow_for_collateral( + collateral_amount in 0..=u32::MAX as u64, + collateral_exchange_rate in collateral_exchange_rate_range(), + token_conversion_rate in 1..=u64::MAX, + loan_to_value_ratio in 1..100u8, + ) { + let total_liquidity = 1_000_000; + let collateral_token_supply = collateral_exchange_rate + .liquidity_to_collateral(total_liquidity)?; + let reserve = Reserve { + collateral: ReserveCollateral { + mint_total_supply: collateral_token_supply, + ..ReserveCollateral::default() + }, + liquidity: ReserveLiquidity { + available_amount: total_liquidity, + ..ReserveLiquidity::default() + }, + config: ReserveConfig { + loan_to_value_ratio, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); + let borrow_amount = reserve.allowed_borrow_for_collateral( + collateral_amount, + MockConverter(conversion_rate) + )?; + + // Allowed borrow should be conservatively low and therefore slightly less or equal + // to the precise equivalent for the given collateral. When it is converted back + // to collateral, the returned value should be equal or lower than the original + // collateral amount. + let collateral_amount_lower = reserve.required_collateral_for_borrow( + borrow_amount, + &Pubkey::default(), + MockConverter(Decimal::one().try_div(conversion_rate)?) + )?; + + // After incrementing the allowed borrow, its value should be slightly more or equal + // to the precise equivalent of the original collateral. When it is converted back + // to collateral, the returned value should be equal or higher than the original + // collateral amount since required collateral should be conservatively high. + let collateral_amount_upper = reserve.required_collateral_for_borrow( + borrow_amount + 1, + &Pubkey::default(), + MockConverter(Decimal::one().try_div(conversion_rate)?) + )?; + + // Assert that reversing the calculation returns approx original amount + assert!(collateral_amount >= collateral_amount_lower); + assert!(collateral_amount <= collateral_amount_upper); + } + + #[test] + fn required_collateral_for_borrow( + borrow_amount in 0..=u32::MAX as u64, + collateral_exchange_rate in collateral_exchange_rate_range(), + token_conversion_rate in 1..=u64::MAX, + loan_to_value_ratio in 1..=100u8, + ) { + let total_liquidity = 1_000_000; + let collateral_token_supply = collateral_exchange_rate + .liquidity_to_collateral(total_liquidity)?; + let reserve = Reserve { + collateral: ReserveCollateral { + mint_total_supply: collateral_token_supply, + ..ReserveCollateral::default() + }, + liquidity: ReserveLiquidity { + available_amount: total_liquidity, + ..ReserveLiquidity::default() + }, + config: ReserveConfig { + loan_to_value_ratio, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); + let collateral_amount = reserve.required_collateral_for_borrow( + borrow_amount, + &Pubkey::default(), + MockConverter(conversion_rate) + )?; + + // Required collateral should be conservatively high and therefore slightly more or equal + // to the precise equivalent for the desired borrow amount. When it is converted back + // to borrow, the returned value should be equal or higher than the original + // borrow amount. + let borrow_amount_upper = reserve.allowed_borrow_for_collateral( + collateral_amount, + MockConverter(Decimal::one().try_div(conversion_rate)?) + )?; + + // After decrementing the required collateral, its value should be slightly less or equal + // to the precise equivalent of the original borrow. When it is converted back + // to borrow, the returned value should be equal or lower than the original + // borrow amount since allowed borrow should be conservatively low. + let borrow_amount_lower = reserve.allowed_borrow_for_collateral( + collateral_amount.saturating_sub(1), + MockConverter(Decimal::one().try_div(conversion_rate)?) + )?; + + // Assert that reversing the calculation returns approx original amount + assert!(borrow_amount >= borrow_amount_lower); + assert!(borrow_amount <= borrow_amount_upper); + } + + #[test] + fn current_utilization_rate( + total_liquidity in 0..=MAX_LIQUIDITY, + borrowed_percent in 0..=WAD, + ) { + let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; + let liquidity = ReserveLiquidity { + borrowed_amount_wads, + available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?, + ..ReserveLiquidity::default() + }; + + let current_rate = liquidity.utilization_rate()?; + assert!(current_rate <= Rate::one()); + } + + #[test] + fn collateral_exchange_rate( + total_liquidity in 0..=MAX_LIQUIDITY, + borrowed_percent in 0..=WAD, + collateral_multiplier in 0..=(5*WAD), + borrow_rate in 0..=u8::MAX, + ) { + let borrowed_liquidity_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?; + let available_liquidity = total_liquidity - borrowed_liquidity_wads.try_round_u64()?; + let mint_total_supply = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(collateral_multiplier))?.try_round_u64()?; + + let mut reserve = Reserve { + collateral: ReserveCollateral { + mint_total_supply, + ..ReserveCollateral::default() + }, + liquidity: ReserveLiquidity { + borrowed_amount_wads: borrowed_liquidity_wads, + available_amount: available_liquidity, + ..ReserveLiquidity::default() + }, + config: ReserveConfig { + min_borrow_rate: borrow_rate, + optimal_borrow_rate: borrow_rate, + optimal_utilization_rate: 100, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + let exchange_rate = reserve.collateral_exchange_rate()?; + assert!(exchange_rate.0.to_scaled_val() <= 5u128 * WAD as u128); + + // After interest accrual, total liquidity increases and collateral are worth more + reserve.accrue_interest(1)?; + + let new_exchange_rate = reserve.collateral_exchange_rate()?; + if borrow_rate > 0 && total_liquidity > 0 && borrowed_percent > 0 { + assert!(new_exchange_rate.0 < exchange_rate.0); + } else { + assert_eq!(new_exchange_rate.0, exchange_rate.0); + } + } + + #[test] + fn compound_interest( + slots_elapsed in 0..=SLOTS_PER_YEAR, + borrow_rate in 0..=u8::MAX, + ) { + let mut reserve = Reserve::default(); + let borrow_rate = Rate::from_percent(borrow_rate); + + // Simulate running for max 1000 years, assuming that interest is + // compounded at least once a year + for _ in 0..1000 { + reserve.compound_interest(borrow_rate, slots_elapsed)?; + reserve.cumulative_borrow_rate_wads.to_scaled_val()?; + } + } + + #[test] + fn reserve_accrue_interest( + slots_elapsed in 0..=SLOTS_PER_YEAR, + borrowed_liquidity in 0..=u64::MAX, + borrow_rate in 0..=u8::MAX, + ) { + let borrowed_amount_wads = Decimal::from(borrowed_liquidity); + let mut reserve = Reserve { + liquidity: ReserveLiquidity { + borrowed_amount_wads, + ..ReserveLiquidity::default() + }, + config: ReserveConfig { + max_borrow_rate: borrow_rate, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + reserve.accrue_interest(slots_elapsed)?; + + if borrow_rate > 0 && slots_elapsed > 0 { + assert!(reserve.liquidity.borrowed_amount_wads > borrowed_amount_wads); + } else { + assert!(reserve.liquidity.borrowed_amount_wads == borrowed_amount_wads); + } + } + + #[test] + fn borrow_fee_calculation( + borrow_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails + host_fee_percentage in 0..=100u8, + borrow_amount in 3..=u64::MAX, // start at 3 to ensure calculation success + // 0, 1, and 2 are covered in the minimum tests + ) { + let fees = ReserveFees { + borrow_fee_wad, + host_fee_percentage, + }; + let (total_fee, host_fee) = fees.calculate_borrow_fees(borrow_amount)?; + + // The total fee can't be greater than the amount borrowed, as long + // as amount borrowed is greater than 2. + // At a borrow amount of 2, we can get a total fee of 2 if a host + // fee is also specified. + assert!(total_fee <= borrow_amount); + + // the host fee can't be greater than the total fee + assert!(host_fee <= total_fee); + + // for all fee rates greater than 0, we must have some fee + if borrow_fee_wad > 0 { + assert!(total_fee > 0); + } + + if host_fee_percentage == 100 { + // if the host fee percentage is maxed at 100%, it should get all the fee + assert_eq!(host_fee, total_fee); + } + + // if there's a host fee and some borrow fee, host fee must be greater than 0 + if host_fee_percentage > 0 && borrow_fee_wad > 0 { + assert!(host_fee > 0); + } else { + assert_eq!(host_fee, 0); + } + } + } + + #[test] + fn liquidate_amount_too_small() { + let conversion_rate = Decimal::from_scaled_val(PERCENT_SCALER as u128); // 1% + let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); + let collateral_reserve_config = &ReserveConfig { + liquidation_threshold: 80u8, + liquidation_bonus: 5u8, + ..ReserveConfig::default() + }; + + let obligation = Obligation { + deposited_collateral_tokens: 1, + borrowed_liquidity_wads: Decimal::from(100u64), + ..Obligation::default() + }; + + let liquidate_result = Reserve::_liquidate_obligation( + &obligation, + 1u64, // converts to 0.01 collateral + &Pubkey::default(), + collateral_exchange_rate, + collateral_reserve_config, + MockConverter(conversion_rate), + ); + + assert_eq!( + liquidate_result.unwrap_err(), + LendingError::LiquidationTooSmall.into() + ); + } + + #[test] + fn liquidate_dust_obligation() { + let conversion_rate = Decimal::one(); + let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); + let collateral_reserve_config = &ReserveConfig { + liquidation_threshold: 80u8, + liquidation_bonus: 5u8, + ..ReserveConfig::default() + }; + + let obligation = Obligation { + deposited_collateral_tokens: 1, + borrowed_liquidity_wads: Decimal::one() + .try_add(Decimal::from_scaled_val(1u128)) + .unwrap(), + ..Obligation::default() + }; + + let liquidate_result = Reserve::_liquidate_obligation( + &obligation, + 2, + &Pubkey::default(), + collateral_exchange_rate, + collateral_reserve_config, + MockConverter(conversion_rate), + ) + .unwrap(); + + assert_eq!( + liquidate_result.repay_amount, + obligation.borrowed_liquidity_wads.try_ceil_u64().unwrap() + ); + assert_eq!( + liquidate_result.withdraw_amount, + obligation.deposited_collateral_tokens + ); + assert_eq!( + liquidate_result.settle_amount, + obligation.borrowed_liquidity_wads + ); + } + + #[test] + fn borrow_fee_calculation_min_host() { + let fees = ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 20, + }; + + // only 2 tokens borrowed, get error + let err = fees.calculate_borrow_fees(2).unwrap_err(); + assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 3 tokens + + // only 1 token borrowed, get error + let err = fees.calculate_borrow_fees(1).unwrap_err(); + assert_eq!(err, LendingError::BorrowTooSmall.into()); + + // 0 amount borrowed, 0 fee + let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); + assert_eq!(total_fee, 0); + assert_eq!(host_fee, 0); + } + + #[test] + fn borrow_fee_calculation_min_no_host() { + let fees = ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 0, + }; + + // only 2 tokens borrowed, ok + let (total_fee, host_fee) = fees.calculate_borrow_fees(2).unwrap(); + assert_eq!(total_fee, 1); + assert_eq!(host_fee, 0); + + // only 1 token borrowed, get error + let err = fees.calculate_borrow_fees(1).unwrap_err(); + assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 2 tokens + + // 0 amount borrowed, 0 fee + let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); + assert_eq!(total_fee, 0); + assert_eq!(host_fee, 0); + } + + #[test] + fn borrow_fee_calculation_host() { + let fees = ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 20, + }; + + let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); + + assert_eq!(total_fee, 10); // 1% of 1000 + assert_eq!(host_fee, 2); // 20% of 10 + } + + #[test] + fn borrow_fee_calculation_no_host() { + let fees = ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 0, + }; + + let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); + + assert_eq!(total_fee, 10); // 1% of 1000 + assert_eq!(host_fee, 0); // 0 host fee + } +} From 9190d021200f2cefbd6f45beed1ecb012cbd0115 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Wed, 28 Apr 2021 15:13:45 -0500 Subject: [PATCH 189/191] unit tests passing --- token-lending/program/src/state/obligation.rs | 117 +++---- token-lending/program/src/state/reserve.rs | 308 +----------------- 2 files changed, 65 insertions(+), 360 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index ce5e41b1e96..7363c2c0fae 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -496,7 +496,6 @@ impl Pack for Obligation { } } -// @FIXME: test #[cfg(test)] mod test { use super::*; @@ -508,28 +507,28 @@ mod test { #[test] fn obligation_accrue_interest_failure() { assert_eq!( - Obligation { + ObligationLiquidity { cumulative_borrow_rate_wads: Decimal::zero(), - ..Obligation::default() + ..ObligationLiquidity::default() } .accrue_interest(Decimal::one()), Err(LendingError::MathOverflow.into()) ); assert_eq!( - Obligation { + ObligationLiquidity { cumulative_borrow_rate_wads: Decimal::from(2u64), - ..Obligation::default() + ..ObligationLiquidity::default() } .accrue_interest(Decimal::one()), Err(LendingError::NegativeInterestRate.into()) ); assert_eq!( - Obligation { + ObligationLiquidity { cumulative_borrow_rate_wads: Decimal::one(), - borrowed_liquidity_wads: Decimal::from(u64::MAX), - ..Obligation::default() + borrowed_amount_wads: Decimal::from(u64::MAX), + ..ObligationLiquidity::default() } .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), Err(LendingError::MathOverflow.into()) @@ -540,7 +539,7 @@ mod test { prop_compose! { fn cumulative_rates()(rate in 1..=u128::MAX)( current_rate in Just(rate), - max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128) + max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128), ) -> (u128, u128) { (current_rate, max_new_rate) } @@ -550,89 +549,81 @@ mod test { // Creates liquidity amounts (repay, borrow) where repay < borrow prop_compose! { - fn repay_partial_amounts()(repay in 1..=u64::MAX)( - liquidity_amount in Just(repay), - borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED - ) -> (u64, u128) { - (liquidity_amount, borrowed_liquidity) + fn repay_partial_amounts()(amount in 1..=u64::MAX)( + repay_amount in Just(WAD as u128 * amount as u128), + borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED, + ) -> (u128, u128) { + (repay_amount, borrowed_amount) } } // Creates liquidity amounts (repay, borrow) where repay >= borrow prop_compose! { - fn repay_full_amounts()(repay in 1..=u64::MAX)( - liquidity_amount in Just(repay), - borrowed_liquidity in 0..=(WAD as u128 * repay as u128) - ) -> (u64, u128) { - (liquidity_amount, borrowed_liquidity) - } - } - - // Creates collateral amounts (collateral, obligation tokens) where c <= ot - prop_compose! { - fn collateral_amounts()(collateral in 1..=u64::MAX)( - deposited_collateral_tokens in Just(collateral), - obligation_tokens in collateral..=u64::MAX - ) -> (u64, u64) { - (deposited_collateral_tokens, obligation_tokens) + fn repay_full_amounts()(amount in 1..=u64::MAX)( + repay_amount in Just(WAD as u128 * amount as u128), + ) -> (u128, u128) { + (repay_amount, repay_amount) } } proptest! { #[test] fn repay_partial( - (liquidity_amount, borrowed_liquidity) in repay_partial_amounts(), - (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), + (repay_amount, borrowed_amount) in repay_partial_amounts(), ) { - let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); - let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() }; - - let repay_result = state.repay(liquidity_amount, obligation_tokens)?; - assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); - assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens); - assert!(repay_result.obligation_token_amount < obligation_tokens); - assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads); - assert!(state.borrowed_liquidity_wads > Decimal::zero()); - assert!(state.deposited_collateral_tokens > 0); - - let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?; - let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?; - assert!(obligation_token_rate <= collateral_withdraw_rate); + let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount); + let repay_amount_wads = Decimal::from_scaled_val(repay_amount); + let mut obligation = Obligation { + borrows: vec![ObligationLiquidity { + borrowed_amount_wads, + ..ObligationLiquidity::default() + }], + ..Obligation::default() + }; + + obligation.repay(repay_amount_wads, 0)?; + assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads); + assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero()); } #[test] fn repay_full( - (liquidity_amount, borrowed_liquidity) in repay_full_amounts(), - (deposited_collateral_tokens, obligation_tokens) in collateral_amounts(), + (repay_amount, borrowed_amount) in repay_full_amounts(), ) { - let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); - let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() } ; - - let repay_result = state.repay(liquidity_amount, obligation_tokens)?; - assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); - assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens); - assert_eq!(repay_result.obligation_token_amount, obligation_tokens); - assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads); - assert_eq!(state.borrowed_liquidity_wads, Decimal::zero()); - assert_eq!(state.deposited_collateral_tokens, 0); + let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount); + let repay_amount_wads = Decimal::from_scaled_val(repay_amount); + let mut obligation = Obligation { + borrows: vec![ObligationLiquidity { + borrowed_amount_wads, + ..ObligationLiquidity::default() + }], + ..Obligation::default() + }; + + obligation.repay(repay_amount_wads, 0)?; + assert_eq!(obligation.borrows.len(), 0); } #[test] fn accrue_interest( - borrowed_liquidity in 0..=u64::MAX, (current_borrow_rate, new_borrow_rate) in cumulative_rates(), + borrowed_amount in 0..=u64::MAX, ) { - let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity); let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; - let mut state = Obligation { cumulative_borrow_rate_wads, borrowed_liquidity_wads, ..Obligation::default() }; + let borrowed_amount_wads = Decimal::from(borrowed_amount); + let mut liquidity = ObligationLiquidity { + cumulative_borrow_rate_wads, + borrowed_amount_wads, + ..ObligationLiquidity::default() + }; let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; - state.accrue_interest(next_cumulative_borrow_rate)?; + liquidity.accrue_interest(next_cumulative_borrow_rate)?; if next_cumulative_borrow_rate > cumulative_borrow_rate_wads { - assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); + assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads); } else { - assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); + assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads); } } } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index a0657a129ab..5e985de14a0 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -901,7 +901,6 @@ impl Pack for Reserve { } } -// @FIXME: unit tests #[cfg(test)] mod test { use super::*; @@ -911,21 +910,6 @@ mod test { const MAX_LIQUIDITY: u64 = u64::MAX / 5; - struct MockConverter(Decimal); - impl TokenConverter for MockConverter { - fn best_price(&mut self, _token_mint: &Pubkey) -> Result { - Ok(self.0) - } - - fn convert( - self, - from_amount: Decimal, - _from_token_mint: &Pubkey, - ) -> Result { - from_amount.try_mul(self.0) - } - } - /// Convert reserve liquidity tokens to the collateral tokens of another reserve fn liquidity_in_other_collateral( liquidity_amount: u64, @@ -981,90 +965,6 @@ mod test { } proptest! { - #[test] - fn unhealthy_obligations_can_be_liquidated( - obligation_collateral in 1..=u64::MAX, - (obligation_ltv, liquidation_threshold) in unhealthy_rates(), - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in token_conversion_rate(), - ) { - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold, - ..ReserveConfig::default() - }; - - // Create unhealthy obligation at target LTV - let collateral_value = collateral_exchange_rate - .decimal_collateral_to_liquidity(Decimal::from(obligation_collateral as u64))? - .try_div(token_conversion_rate)?; - - // Ensure that collateral value fits in u64 - prop_assume!(collateral_value.try_round_u64().is_ok()); - - let borrowed_liquidity_wads = collateral_value - .try_mul(obligation_ltv)? - .try_add(Decimal::from_scaled_val(1u128))? // ensure loan is unhealthy - .max(Decimal::from(2u64)); // avoid dust account closure - - // Ensure that borrow value fits in u64 - prop_assume!(borrowed_liquidity_wads.try_round_u64().is_ok()); - - let obligation = Obligation { - deposited_collateral_tokens: obligation_collateral as u64, - borrowed_liquidity_wads, - ..Obligation::default() - }; - - // Ensure that the token conversion fits in a Decimal - { - let token_converter = MockConverter(token_conversion_rate); - let decimal_repay_amount = Decimal::from(obligation.max_liquidation_amount()?); - // Calculate the amount of collateral that will be received - let receive_liquidity_amount_result = - token_converter.convert(decimal_repay_amount, &Pubkey::default()); - prop_assume!(receive_liquidity_amount_result.is_ok()); - } - - // Liquidate with max amount to ensure obligation can be liquidated - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - u64::MAX, - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(token_conversion_rate) - ); - - let liquidate_result = liquidate_result.unwrap(); - let expected_withdraw_amount = liquidity_in_other_collateral( - liquidate_result.repay_amount, - collateral_exchange_rate, - token_conversion_rate, - )?.min(obligation.deposited_collateral_tokens.into()); - - assert!(liquidate_result.repay_amount > 0); - assert!(liquidate_result.withdraw_amount > 0); - - let min_withdraw_amount = expected_withdraw_amount.try_floor_u64()?; - let max_withdraw_amount = expected_withdraw_amount.try_ceil_u64()?; - let max_repay_amount = obligation.borrowed_liquidity_wads - .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .try_ceil_u64()?; - - assert!(liquidate_result.withdraw_amount >= min_withdraw_amount); - assert!(liquidate_result.withdraw_amount <= max_withdraw_amount); - assert!(liquidate_result.repay_amount <= max_repay_amount); - - let defaulted = liquidate_result.withdraw_amount == obligation.deposited_collateral_tokens; - if defaulted { - assert_eq!(liquidate_result.settle_amount, borrowed_liquidity_wads); - assert!(liquidate_result.repay_amount < liquidate_result.settle_amount.try_floor_u64()?); - } else { - assert_eq!(liquidate_result.settle_amount.try_ceil_u64()?, liquidate_result.repay_amount); - assert!(liquidate_result.withdraw_amount < obligation.deposited_collateral_tokens); - } - } - #[test] fn current_borrow_rate( total_liquidity in 0..=MAX_LIQUIDITY, @@ -1108,119 +1008,6 @@ mod test { } } - #[test] - fn allowed_borrow_for_collateral( - collateral_amount in 0..=u32::MAX as u64, - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in 1..=u64::MAX, - loan_to_value_ratio in 1..100u8, - ) { - let total_liquidity = 1_000_000; - let collateral_token_supply = collateral_exchange_rate - .liquidity_to_collateral(total_liquidity)?; - let reserve = Reserve { - collateral: ReserveCollateral { - mint_total_supply: collateral_token_supply, - ..ReserveCollateral::default() - }, - liquidity: ReserveLiquidity { - available_amount: total_liquidity, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - loan_to_value_ratio, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); - let borrow_amount = reserve.allowed_borrow_for_collateral( - collateral_amount, - MockConverter(conversion_rate) - )?; - - // Allowed borrow should be conservatively low and therefore slightly less or equal - // to the precise equivalent for the given collateral. When it is converted back - // to collateral, the returned value should be equal or lower than the original - // collateral amount. - let collateral_amount_lower = reserve.required_collateral_for_borrow( - borrow_amount, - &Pubkey::default(), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // After incrementing the allowed borrow, its value should be slightly more or equal - // to the precise equivalent of the original collateral. When it is converted back - // to collateral, the returned value should be equal or higher than the original - // collateral amount since required collateral should be conservatively high. - let collateral_amount_upper = reserve.required_collateral_for_borrow( - borrow_amount + 1, - &Pubkey::default(), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // Assert that reversing the calculation returns approx original amount - assert!(collateral_amount >= collateral_amount_lower); - assert!(collateral_amount <= collateral_amount_upper); - } - - #[test] - fn required_collateral_for_borrow( - borrow_amount in 0..=u32::MAX as u64, - collateral_exchange_rate in collateral_exchange_rate_range(), - token_conversion_rate in 1..=u64::MAX, - loan_to_value_ratio in 1..=100u8, - ) { - let total_liquidity = 1_000_000; - let collateral_token_supply = collateral_exchange_rate - .liquidity_to_collateral(total_liquidity)?; - let reserve = Reserve { - collateral: ReserveCollateral { - mint_total_supply: collateral_token_supply, - ..ReserveCollateral::default() - }, - liquidity: ReserveLiquidity { - available_amount: total_liquidity, - ..ReserveLiquidity::default() - }, - config: ReserveConfig { - loan_to_value_ratio, - ..ReserveConfig::default() - }, - ..Reserve::default() - }; - - let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128); - let collateral_amount = reserve.required_collateral_for_borrow( - borrow_amount, - &Pubkey::default(), - MockConverter(conversion_rate) - )?; - - // Required collateral should be conservatively high and therefore slightly more or equal - // to the precise equivalent for the desired borrow amount. When it is converted back - // to borrow, the returned value should be equal or higher than the original - // borrow amount. - let borrow_amount_upper = reserve.allowed_borrow_for_collateral( - collateral_amount, - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // After decrementing the required collateral, its value should be slightly less or equal - // to the precise equivalent of the original borrow. When it is converted back - // to borrow, the returned value should be equal or lower than the original - // borrow amount since allowed borrow should be conservatively low. - let borrow_amount_lower = reserve.allowed_borrow_for_collateral( - collateral_amount.saturating_sub(1), - MockConverter(Decimal::one().try_div(conversion_rate)?) - )?; - - // Assert that reversing the calculation returns approx original amount - assert!(borrow_amount >= borrow_amount_lower); - assert!(borrow_amount <= borrow_amount_upper); - } - #[test] fn current_utilization_rate( total_liquidity in 0..=MAX_LIQUIDITY, @@ -1292,8 +1079,8 @@ mod test { // Simulate running for max 1000 years, assuming that interest is // compounded at least once a year for _ in 0..1000 { - reserve.compound_interest(borrow_rate, slots_elapsed)?; - reserve.cumulative_borrow_rate_wads.to_scaled_val()?; + reserve.liquidity.compound_interest(borrow_rate, slots_elapsed)?; + reserve.liquidity.cumulative_borrow_rate_wads.to_scaled_val()?; } } @@ -1336,7 +1123,7 @@ mod test { borrow_fee_wad, host_fee_percentage, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(borrow_amount)?; + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(borrow_amount), FeeCalculation::Exclusive)?; // The total fee can't be greater than the amount borrowed, as long // as amount borrowed is greater than 2. @@ -1366,79 +1153,6 @@ mod test { } } - #[test] - fn liquidate_amount_too_small() { - let conversion_rate = Decimal::from_scaled_val(PERCENT_SCALER as u128); // 1% - let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold: 80u8, - liquidation_bonus: 5u8, - ..ReserveConfig::default() - }; - - let obligation = Obligation { - deposited_collateral_tokens: 1, - borrowed_liquidity_wads: Decimal::from(100u64), - ..Obligation::default() - }; - - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - 1u64, // converts to 0.01 collateral - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(conversion_rate), - ); - - assert_eq!( - liquidate_result.unwrap_err(), - LendingError::LiquidationTooSmall.into() - ); - } - - #[test] - fn liquidate_dust_obligation() { - let conversion_rate = Decimal::one(); - let collateral_exchange_rate = CollateralExchangeRate(Rate::one()); - let collateral_reserve_config = &ReserveConfig { - liquidation_threshold: 80u8, - liquidation_bonus: 5u8, - ..ReserveConfig::default() - }; - - let obligation = Obligation { - deposited_collateral_tokens: 1, - borrowed_liquidity_wads: Decimal::one() - .try_add(Decimal::from_scaled_val(1u128)) - .unwrap(), - ..Obligation::default() - }; - - let liquidate_result = Reserve::_liquidate_obligation( - &obligation, - 2, - &Pubkey::default(), - collateral_exchange_rate, - collateral_reserve_config, - MockConverter(conversion_rate), - ) - .unwrap(); - - assert_eq!( - liquidate_result.repay_amount, - obligation.borrowed_liquidity_wads.try_ceil_u64().unwrap() - ); - assert_eq!( - liquidate_result.withdraw_amount, - obligation.deposited_collateral_tokens - ); - assert_eq!( - liquidate_result.settle_amount, - obligation.borrowed_liquidity_wads - ); - } - #[test] fn borrow_fee_calculation_min_host() { let fees = ReserveFees { @@ -1447,15 +1161,15 @@ mod test { }; // only 2 tokens borrowed, get error - let err = fees.calculate_borrow_fees(2).unwrap_err(); + let err = fees.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive).unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 3 tokens // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(1).unwrap_err(); + let err = fees.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive).unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive).unwrap(); assert_eq!(total_fee, 0); assert_eq!(host_fee, 0); } @@ -1468,16 +1182,16 @@ mod test { }; // only 2 tokens borrowed, ok - let (total_fee, host_fee) = fees.calculate_borrow_fees(2).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive).unwrap(); assert_eq!(total_fee, 1); assert_eq!(host_fee, 0); // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(1).unwrap_err(); + let err = fees.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive).unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 2 tokens // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(0).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive).unwrap(); assert_eq!(total_fee, 0); assert_eq!(host_fee, 0); } @@ -1489,7 +1203,7 @@ mod test { host_fee_percentage: 20, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive).unwrap(); assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 2); // 20% of 10 @@ -1502,7 +1216,7 @@ mod test { host_fee_percentage: 0, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(1000).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive).unwrap(); assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 0); // 0 host fee From b3b0daf9ab8c4af925c7982b1b97198d942c2784 Mon Sep 17 00:00:00 2001 From: jordansexton Date: Wed, 28 Apr 2021 15:20:06 -0500 Subject: [PATCH 190/191] fmt / clippy --- token-lending/program/src/state/obligation.rs | 6 ++-- token-lending/program/src/state/reserve.rs | 32 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/program/src/state/obligation.rs index 7363c2c0fae..e71f3b3ceef 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/program/src/state/obligation.rs @@ -511,7 +511,7 @@ mod test { cumulative_borrow_rate_wads: Decimal::zero(), ..ObligationLiquidity::default() } - .accrue_interest(Decimal::one()), + .accrue_interest(Decimal::one()), Err(LendingError::MathOverflow.into()) ); @@ -520,7 +520,7 @@ mod test { cumulative_borrow_rate_wads: Decimal::from(2u64), ..ObligationLiquidity::default() } - .accrue_interest(Decimal::one()), + .accrue_interest(Decimal::one()), Err(LendingError::NegativeInterestRate.into()) ); @@ -530,7 +530,7 @@ mod test { borrowed_amount_wads: Decimal::from(u64::MAX), ..ObligationLiquidity::default() } - .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), + .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), Err(LendingError::MathOverflow.into()) ); } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 5e985de14a0..b7eea7f9d21 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -1161,15 +1161,21 @@ mod test { }; // only 2 tokens borrowed, get error - let err = fees.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive).unwrap_err(); + let err = fees + .calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive) + .unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 3 tokens // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive).unwrap_err(); + let err = fees + .calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive) + .unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive).unwrap(); + let (total_fee, host_fee) = fees + .calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive) + .unwrap(); assert_eq!(total_fee, 0); assert_eq!(host_fee, 0); } @@ -1182,16 +1188,22 @@ mod test { }; // only 2 tokens borrowed, ok - let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive).unwrap(); + let (total_fee, host_fee) = fees + .calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive) + .unwrap(); assert_eq!(total_fee, 1); assert_eq!(host_fee, 0); // only 1 token borrowed, get error - let err = fees.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive).unwrap_err(); + let err = fees + .calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive) + .unwrap_err(); assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 2 tokens // 0 amount borrowed, 0 fee - let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive).unwrap(); + let (total_fee, host_fee) = fees + .calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive) + .unwrap(); assert_eq!(total_fee, 0); assert_eq!(host_fee, 0); } @@ -1203,7 +1215,9 @@ mod test { host_fee_percentage: 20, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive).unwrap(); + let (total_fee, host_fee) = fees + .calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive) + .unwrap(); assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 2); // 20% of 10 @@ -1216,7 +1230,9 @@ mod test { host_fee_percentage: 0, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive).unwrap(); + let (total_fee, host_fee) = fees + .calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive) + .unwrap(); assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 0); // 0 host fee From 357e13b21de1fe7c63303abb33429b2a8e8eff9f Mon Sep 17 00:00:00 2001 From: jordansexton Date: Wed, 28 Apr 2021 15:35:41 -0500 Subject: [PATCH 191/191] remove unused function for clippy --- token-lending/program/src/state/reserve.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index b7eea7f9d21..7d21040ccb2 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -910,17 +910,6 @@ mod test { const MAX_LIQUIDITY: u64 = u64::MAX / 5; - /// Convert reserve liquidity tokens to the collateral tokens of another reserve - fn liquidity_in_other_collateral( - liquidity_amount: u64, - collateral_exchange_rate: CollateralExchangeRate, - conversion_rate: Decimal, - ) -> Result { - collateral_exchange_rate.decimal_liquidity_to_collateral( - Decimal::from(liquidity_amount).try_mul(conversion_rate)?, - ) - } - // Creates rates (min, opt, max) where 0 <= min <= opt <= max <= MAX prop_compose! { fn borrow_rates()(optimal_rate in 0..=u8::MAX)(