lending: Add InitObligation instruction (#1088)

* lending: Add InitObligation instruction

* fix: accrue interest on deposit to correct exchange rate
This commit is contained in:
Justin Starry 2021-01-19 16:58:04 +08:00 committed by GitHub
parent fdf2f1f909
commit bbd4c63b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 616 additions and 190 deletions

View File

@ -63,6 +63,21 @@ pub enum LendingInstruction {
config: ReserveConfig,
},
/// Initializes a new loan obligation.
///
/// 0. `[]` Deposit reserve account.
/// 1. `[writable]` 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
InitObligation,
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership
/// of the reserve liquidity pool.
///
@ -113,17 +128,15 @@ pub enum LendingInstruction {
/// 7. `[writable]` Obligation
/// 8. `[writable]` Obligation token mint
/// 9. `[writable]` Obligation token output
/// 10 `[]` Obligation token owner
/// 11 `[]` Lending market account.
/// 12 `[]` Derived lending market authority.
/// 13 `[]` User transfer authority ($authority).
/// 14 `[]` Dex market
/// 15 `[]` Dex market order book side
/// 16 `[]` Temporary memory
/// 17 `[]` Clock sysvar
/// 18 `[]` Rent sysvar
/// 19 '[]` Token program id
/// 20 `[optional, writable]` Deposit reserve collateral host fee receiver account.
/// 10 `[]` Lending market account.
/// 11 `[]` Derived lending market authority.
/// 12 `[]` 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 {
// TODO: slippage constraint
/// Amount whose usage depends on `amount_type`
@ -218,15 +231,16 @@ impl LendingInstruction {
},
}
}
2 => {
2 => Self::InitObligation,
3 => {
let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
Self::DepositReserveLiquidity { liquidity_amount }
}
3 => {
4 => {
let (collateral_amount, _rest) = Self::unpack_u64(rest)?;
Self::WithdrawReserveLiquidity { collateral_amount }
}
4 => {
5 => {
let (amount, rest) = Self::unpack_u64(rest)?;
let (amount_type, _rest) = Self::unpack_u8(rest)?;
let amount_type = BorrowAmountType::from_u8(amount_type)
@ -236,11 +250,11 @@ impl LendingInstruction {
amount_type,
}
}
5 => {
6 => {
let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
Self::RepayReserveLiquidity { liquidity_amount }
}
6 => {
7 => {
let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
Self::LiquidateObligation { liquidity_amount }
}
@ -324,28 +338,31 @@ 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::InitObligation => {
buf.push(2);
}
Self::DepositReserveLiquidity { liquidity_amount } => {
buf.push(3);
buf.extend_from_slice(&liquidity_amount.to_le_bytes());
}
Self::WithdrawReserveLiquidity { collateral_amount } => {
buf.push(3);
buf.push(4);
buf.extend_from_slice(&collateral_amount.to_le_bytes());
}
Self::BorrowReserveLiquidity {
amount,
amount_type,
} => {
buf.push(4);
buf.push(5);
buf.extend_from_slice(&amount.to_le_bytes());
buf.extend_from_slice(&amount_type.to_u8().unwrap().to_le_bytes());
}
Self::RepayReserveLiquidity { liquidity_amount } => {
buf.push(5);
buf.push(6);
buf.extend_from_slice(&liquidity_amount.to_le_bytes());
}
Self::LiquidateObligation { liquidity_amount } => {
buf.push(6);
buf.push(7);
buf.extend_from_slice(&liquidity_amount.to_le_bytes());
}
}
@ -429,6 +446,40 @@ pub fn init_reserve(
}
}
/// Creates an 'InitObligation' instruction.
#[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,
) -> 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(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,
data: LendingInstruction::InitObligation.pack(),
}
}
/// Creates a 'DepositReserveLiquidity' instruction.
#[allow(clippy::too_many_arguments)]
pub fn deposit_reserve_liquidity(
@ -512,7 +563,6 @@ pub fn borrow_reserve_liquidity(
obligation_pubkey: Pubkey,
obligation_token_mint_pubkey: Pubkey,
obligation_token_output_pubkey: Pubkey,
obligation_token_owner_pubkey: Pubkey,
dex_market_pubkey: Pubkey,
dex_market_order_book_side_pubkey: Pubkey,
memory_pubkey: Pubkey,
@ -529,7 +579,6 @@ pub fn borrow_reserve_liquidity(
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(user_transfer_authority_pubkey, true),
@ -537,7 +586,6 @@ pub fn borrow_reserve_liquidity(
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(sysvar::rent::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
];
if let Some(deposit_reserve_collateral_host_pubkey) = deposit_reserve_collateral_host_pubkey {

View File

@ -6,8 +6,8 @@ use crate::{
instruction::{BorrowAmountType, LendingInstruction},
math::{Decimal, Rate, TryAdd, TryMul, TrySub, WAD},
state::{
LendingMarket, NewReserveParams, Obligation, RepayResult, Reserve, ReserveCollateral,
ReserveConfig, ReserveLiquidity, PROGRAM_VERSION,
LendingMarket, NewObligationParams, NewReserveParams, Obligation, RepayResult, Reserve,
ReserveCollateral, ReserveConfig, ReserveLiquidity, PROGRAM_VERSION,
},
};
use num_traits::FromPrimitive;
@ -44,6 +44,10 @@ pub fn process_instruction(
msg!("Instruction: Init Reserve");
process_init_reserve(program_id, liquidity_amount, config, accounts)
}
LendingInstruction::InitObligation => {
msg!("Instruction: Init Obligation");
process_init_obligation(program_id, accounts)
}
LendingInstruction::DepositReserveLiquidity { liquidity_amount } => {
msg!("Instruction: Deposit");
process_deposit(program_id, liquidity_amount, accounts)
@ -292,6 +296,105 @@ 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 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)?;
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)?;
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 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>(obligation_info)?;
// accrue interest and update rates
borrow_reserve.accrue_interest(clock.slot)?;
let cumulative_borrow_rate = borrow_reserve.cumulative_borrow_rate_wads;
Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?;
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,
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],
];
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());
}
// 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(())
}
fn process_deposit(
program_id: &Pubkey,
liquidity_amount: u64,
@ -492,7 +595,6 @@ fn process_borrow(
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 user_transfer_authority_info = next_account_info(account_info_iter)?;
@ -500,8 +602,6 @@ fn process_borrow(
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_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())?;
@ -575,10 +675,37 @@ fn process_borrow(
}
}
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");
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());
}
unpack_mint(&obligation_token_mint_info.data.borrow())?;
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());
}
// accrue interest and update rates
borrow_reserve.accrue_interest(clock.slot)?;
deposit_reserve.accrue_interest(clock.slot)?;
let cumulative_borrow_rate = borrow_reserve.cumulative_borrow_rate_wads;
obligation.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?;
let mut trade_simulator = TradeSimulator::new(
dex_market_info,
@ -603,45 +730,14 @@ fn process_borrow(
borrow_reserve.liquidity.borrow(borrow_amount)?;
let obligation_mint_decimals = deposit_reserve.liquidity.mint_decimals;
obligation.borrowed_liquidity_wads = obligation
.borrowed_liquidity_wads
.try_add(Decimal::from(borrow_amount))?;
obligation.deposited_collateral_tokens += collateral_deposit_amount;
Reserve::pack(deposit_reserve, &mut deposit_reserve_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 obligation = Obligation::unpack_unchecked(&obligation_info.data.borrow())?;
let reusing_obligation = obligation.is_initialized();
if reusing_obligation {
if &obligation.token_mint != obligation_token_mint_info.key {
msg!("Obligation token mint input doesn't match existing obligation token mint");
return Err(LendingError::InvalidAccountInput.into());
}
if &obligation.borrow_reserve != borrow_reserve_info.key {
msg!("Borrow reserve input doesn't match existing obligation borrow reserve");
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());
}
obligation.accrue_interest(cumulative_borrow_rate)?;
obligation.borrowed_liquidity_wads = obligation
.borrowed_liquidity_wads
.try_add(Decimal::from(borrow_amount))?;
obligation.deposited_collateral_tokens += collateral_deposit_amount;
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
} else {
assert_rent_exempt(rent, obligation_info)?;
let mut new_obligation = obligation;
new_obligation.version = PROGRAM_VERSION;
new_obligation.deposited_collateral_tokens = collateral_deposit_amount;
new_obligation.collateral_reserve = *deposit_reserve_info.key;
new_obligation.cumulative_borrow_rate_wads = cumulative_borrow_rate;
new_obligation.borrowed_liquidity_wads = Decimal::from(borrow_amount);
new_obligation.borrow_reserve = *borrow_reserve_info.key;
new_obligation.token_mint = *obligation_token_mint_info.key;
Obligation::pack(new_obligation, &mut obligation_info.data.borrow_mut())?;
}
Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?;
let authority_signer_seeds = &[
lending_market_info.key.as_ref(),
@ -700,44 +796,6 @@ fn process_borrow(
token_program: token_program_id.clone(),
})?;
if !reusing_obligation {
// 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(),
})?;
}
let obligation_token_output = if reusing_obligation {
let obligation_token_output =
Token::unpack_unchecked(&obligation_token_output_info.data.borrow())?;
if obligation_token_output.is_initialized() {
Some(obligation_token_output)
} else {
None
}
} else {
None
};
if let Some(token_output) = obligation_token_output {
if &token_output.owner != obligation_token_owner_info.key {
return Err(LendingError::ObligationTokenOwnerMismatch.into());
}
} else {
// 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(),
})?;
}
// mint obligation tokens to output account
spl_token_mint_to(TokenMintToParams {
mint: obligation_token_mint_info.clone(),

View File

@ -31,6 +31,26 @@ pub struct Obligation {
}
impl Obligation {
/// Create new obligation
pub fn new(params: NewObligationParams) -> Self {
let NewObligationParams {
collateral_reserve,
borrow_reserve,
token_mint,
cumulative_borrow_rate_wads,
} = params;
Self {
version: PROGRAM_VERSION,
deposited_collateral_tokens: 0,
collateral_reserve,
cumulative_borrow_rate_wads,
borrowed_liquidity_wads: Decimal::zero(),
borrow_reserve,
token_mint,
}
}
/// Accrue interest
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> Result<(), ProgramError> {
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
@ -104,6 +124,18 @@ pub struct RepayResult {
pub integer_repay_amount: u64,
}
/// Create new obligation
pub struct NewObligationParams {
/// Collateral reserve address
pub collateral_reserve: Pubkey,
/// Borrow reserve address
pub borrow_reserve: Pubkey,
/// Obligation token mint address
pub token_mint: Pubkey,
/// Borrow rate used for calculating interest.
pub cumulative_borrow_rate_wads: Decimal,
}
impl Sealed for Obligation {}
impl IsInitialized for Obligation {
fn is_initialized(&self) -> bool {

View File

@ -7,6 +7,7 @@ use solana_program_test::*;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use spl_token_lending::{
instruction::BorrowAmountType,
math::Decimal,
processor::process_instruction,
state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR},
};
@ -34,6 +35,9 @@ async fn test_borrow_quote_currency() {
processor!(process_instruction),
);
// limit to track compute unit increase
test.set_bpf_compute_max_units(185_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);
@ -42,6 +46,11 @@ async fn test_borrow_quote_currency() {
let mut reserve_config = TEST_RESERVE_CONFIG;
reserve_config.loan_to_value_ratio = 80;
// Configure reserve to a fixed borrow rate of 1%
reserve_config.min_borrow_rate = 1;
reserve_config.optimal_borrow_rate = 1;
reserve_config.optimal_utilization_rate = 100;
let usdc_reserve = add_reserve(
&mut test,
&user_accounts_owner,
@ -62,6 +71,7 @@ async fn test_borrow_quote_currency() {
&lending_market,
AddReserveArgs {
slots_elapsed: SLOTS_PER_YEAR,
borrow_amount: 20, // slightly increase collateral value w/ interest accrual
dex_market_pubkey: Some(sol_usdc_dex_market.pubkey),
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
liquidity_mint_pubkey: spl_token::native_mint::id(),
@ -71,6 +81,18 @@ async fn test_borrow_quote_currency() {
},
);
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 =
@ -82,7 +104,7 @@ async fn test_borrow_quote_currency() {
assert_eq!(collateral_supply, 0);
let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * SOL_COLLATERAL_AMOUNT_LAMPORTS;
let obligation = lending_market
lending_market
.borrow(
&mut banks_client,
&payer,
@ -93,7 +115,7 @@ async fn test_borrow_quote_currency() {
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
amount: collateral_deposit_amount,
user_accounts_owner: &user_accounts_owner,
obligation: None,
obligation: &usdc_obligation,
},
)
.await;
@ -121,9 +143,9 @@ async fn test_borrow_quote_currency() {
borrow_reserve: &usdc_reserve,
dex_market: &sol_usdc_dex_market,
borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount,
amount: USDC_BORROW_AMOUNT_FRACTIONAL,
amount: borrow_amount,
user_accounts_owner: &user_accounts_owner,
obligation: Some(obligation),
obligation: &usdc_obligation,
},
)
.await;
@ -132,9 +154,17 @@ async fn test_borrow_quote_currency() {
get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await;
assert_eq!(borrow_amount, 2 * USDC_BORROW_AMOUNT_FRACTIONAL);
// The SOL reserve accumulates interest which slightly increases the value
// of collateral, resulting in slightly less collateral required for new loans
const COLLATERAL_EPSILON: u64 = 1;
let user_collateral_balance =
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await;
assert_eq!(user_collateral_balance, COLLATERAL_EPSILON);
let collateral_deposited = 2 * collateral_deposit_amount - COLLATERAL_EPSILON;
let (total_fee, host_fee) = TEST_RESERVE_CONFIG
.fees
.calculate_borrow_fees(2 * collateral_deposit_amount)
.calculate_borrow_fees(collateral_deposited)
.unwrap();
assert!(total_fee > 0);
@ -142,7 +172,7 @@ async fn test_borrow_quote_currency() {
let collateral_supply =
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee);
assert_eq!(collateral_supply, collateral_deposited - total_fee);
let fee_balance =
get_token_balance(&mut banks_client, sol_reserve.collateral_fees_receiver).await;
@ -160,18 +190,25 @@ async fn test_borrow_base_currency() {
// $2.210, 212.5 SOL
//
// Borrow amount = 600 SOL
// Collateral amount = 2.21 * 212.5 + 2.211 * 300 + 2.212 * 87.5 = 1,329.475 USDC
// Collateral amount = 2.21 * 212.5 + 2.211 * 300 + 2.212 * 87.5 = 1,326.475 USDC
const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 600 * LAMPORTS_TO_SOL;
const USDC_COLLATERAL_LAMPORTS: u64 = 1_326_475_000;
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 5000 * LAMPORTS_TO_SOL;
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 2 * USDC_COLLATERAL_LAMPORTS;
// The USDC collateral reserve accumulates interest which slightly increases
// the value of collateral, resulting in additional borrow power
const INTEREST_EPSILON: u64 = 2;
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(188_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);
@ -180,12 +217,18 @@ async fn test_borrow_base_currency() {
let mut reserve_config = TEST_RESERVE_CONFIG;
reserve_config.loan_to_value_ratio = 100;
// Configure reserve to a fixed borrow rate of 1%
reserve_config.min_borrow_rate = 1;
reserve_config.optimal_borrow_rate = 1;
reserve_config.optimal_utilization_rate = 100;
let usdc_reserve = add_reserve(
&mut test,
&user_accounts_owner,
&lending_market,
AddReserveArgs {
slots_elapsed: SLOTS_PER_YEAR,
borrow_amount: 1, // slightly increase collateral value w/ interest accrual
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
liquidity_mint_pubkey: usdc_mint.pubkey,
liquidity_mint_decimals: usdc_mint.decimals,
@ -209,6 +252,18 @@ async fn test_borrow_base_currency() {
},
);
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 =
@ -220,7 +275,7 @@ async fn test_borrow_base_currency() {
assert_eq!(collateral_supply, 0);
let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * USDC_COLLATERAL_LAMPORTS;
let obligation = lending_market
lending_market
.borrow(
&mut banks_client,
&payer,
@ -231,14 +286,14 @@ async fn test_borrow_base_currency() {
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
amount: collateral_deposit_amount,
user_accounts_owner: &user_accounts_owner,
obligation: None,
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);
assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS + INTEREST_EPSILON);
let borrow_fees = TEST_RESERVE_CONFIG
.fees
@ -259,16 +314,19 @@ async fn test_borrow_base_currency() {
borrow_reserve: &sol_reserve,
dex_market: &sol_usdc_dex_market,
borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount,
amount: borrow_amount,
amount: borrow_amount - INTEREST_EPSILON,
user_accounts_owner: &user_accounts_owner,
obligation: Some(obligation),
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);
assert_eq!(
borrow_amount,
2 * SOL_BORROW_AMOUNT_LAMPORTS + INTEREST_EPSILON
);
let (mut total_fee, mut host_fee) = TEST_RESERVE_CONFIG
.fees

View File

@ -6,6 +6,7 @@ use helpers::*;
use solana_sdk::signature::Keypair;
use spl_token_lending::{
instruction::BorrowAmountType,
math::Decimal,
state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION},
};
@ -89,6 +90,42 @@ async fn test_success() {
},
);
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
@ -156,7 +193,7 @@ async fn test_success() {
);
// Borrow USDC with SOL collateral
let obligation = lending_market
lending_market
.borrow(
&mut banks_client,
&payer,
@ -167,7 +204,7 @@ async fn test_success() {
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS,
user_accounts_owner: &user_accounts_owner,
obligation: None,
obligation: &usdc_obligation,
},
)
.await;
@ -187,7 +224,7 @@ async fn test_success() {
/ 100,
),
user_accounts_owner: &user_accounts_owner,
obligation: Some(obligation),
obligation: &usdc_obligation,
},
)
.await;
@ -224,7 +261,7 @@ async fn test_success() {
/ 100,
),
user_accounts_owner: &user_accounts_owner,
obligation: None,
obligation: &sol_obligation,
},
)
.await;
@ -246,7 +283,7 @@ async fn test_success() {
/ 100,
),
user_accounts_owner: &user_accounts_owner,
obligation: None,
obligation: &srm_obligation,
},
)
.await;

View File

@ -15,8 +15,8 @@ use spl_token::{
};
use spl_token_lending::{
instruction::{
borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_reserve,
liquidate_obligation, BorrowAmountType,
borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_obligation,
init_reserve, liquidate_obligation, BorrowAmountType,
},
math::Decimal,
processor::process_instruction,
@ -209,9 +209,11 @@ pub fn add_obligation(
);
TestObligation {
keypair: obligation_keypair,
pubkey: obligation_pubkey,
token_mint: token_mint_pubkey,
token_account: token_account_pubkey,
borrow_reserve: borrow_reserve.pubkey,
collateral_reserve: collateral_reserve.pubkey,
}
}
@ -429,7 +431,7 @@ pub struct BorrowArgs<'a> {
pub amount: u64,
pub dex_market: &'a TestDexMarket,
pub user_accounts_owner: &'a Keypair,
pub obligation: Option<TestObligation>,
pub obligation: &'a TestObligation,
}
pub struct LiquidateArgs<'a> {
@ -580,7 +582,7 @@ impl TestLendingMarket {
repay_reserve.liquidity_supply,
withdraw_reserve.pubkey,
withdraw_reserve.collateral_supply,
obligation.keypair.pubkey(),
obligation.pubkey,
self.pubkey,
self.authority,
user_transfer_authority.pubkey(),
@ -610,8 +612,7 @@ impl TestLendingMarket {
banks_client: &mut BanksClient,
payer: &Keypair,
args: BorrowArgs<'_>,
) -> TestObligation {
let rent = banks_client.get_rent().await.unwrap();
) {
let memory_keypair = Keypair::new();
let user_transfer_authority = Keypair::new();
@ -637,60 +638,6 @@ impl TestLendingMarket {
get_token_balance(banks_client, deposit_reserve.user_collateral_account).await
};
let obligation = if let Some(obligation) = obligation {
obligation
} else {
let obligation_token_mint_keypair = Keypair::new();
let obligation_token_account_keypair = Keypair::new();
let obligation = TestObligation {
keypair: Keypair::new(),
token_mint: obligation_token_mint_keypair.pubkey(),
token_account: obligation_token_account_keypair.pubkey(),
};
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(),
rent.minimum_balance(Obligation::LEN),
Obligation::LEN as u64,
&spl_token_lending::id(),
),
],
Some(&payer.pubkey()),
);
let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap();
transaction.sign(
&vec![
payer,
&obligation.keypair,
&obligation_token_account_keypair,
&obligation_token_mint_keypair,
],
recent_blockhash,
);
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
obligation
};
let mut transaction = Transaction::new_with_payer(
&[
approve(
@ -723,10 +670,9 @@ impl TestLendingMarket {
self.pubkey,
self.authority,
user_transfer_authority.pubkey(),
obligation.keypair.pubkey(),
obligation.pubkey,
obligation.token_mint,
obligation.token_account,
user_accounts_owner.pubkey(),
dex_market.pubkey,
dex_market_orders_pubkey,
memory_keypair.pubkey(),
@ -748,7 +694,6 @@ impl TestLendingMarket {
);
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
obligation
}
pub async fn get_state(&self, banks_client: &mut BanksClient) -> LendingMarket {
@ -1036,21 +981,110 @@ impl TestReserve {
}
}
#[derive(Debug)]
pub struct TestObligation {
pub keypair: Keypair,
pub pubkey: Pubkey,
pub token_mint: Pubkey,
pub token_account: Pubkey,
pub collateral_reserve: Pubkey,
pub borrow_reserve: Pubkey,
}
impl TestObligation {
#[allow(clippy::too_many_arguments)]
pub async fn init(
banks_client: &mut BanksClient,
lending_market: &TestLendingMarket,
deposit_reserve: &TestReserve,
borrow_reserve: &TestReserve,
payer: &Keypair,
user_accounts_owner: &Keypair,
) -> Result<Self, TransactionError> {
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,
};
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(),
rent.minimum_balance(Obligation::LEN),
Obligation::LEN as u64,
&spl_token_lending::id(),
),
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(),
),
],
Some(&payer.pubkey()),
);
let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap();
transaction.sign(
&vec![
payer,
&obligation_keypair,
&obligation_token_account_keypair,
&obligation_token_mint_keypair,
],
recent_blockhash,
);
banks_client
.process_transaction(transaction)
.await
.map_err(|e| e.unwrap())?;
Ok(obligation)
}
pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation {
let obligation_account: Account = banks_client
.get_account(self.keypair.pubkey())
.get_account(self.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);
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);
}
}
pub struct TestQuoteMint {

View File

@ -0,0 +1,159 @@
#![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::init_obligation, math::Decimal,
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(60_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 {
slots_elapsed: SLOTS_PER_YEAR,
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,
)
.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]
async fn test_already_initialized() {
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(14_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 {
slots_elapsed: SLOTS_PER_YEAR,
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(),
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
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(),
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
0,
InstructionError::Custom(LendingError::AlreadyInitialized as u32)
)
);
}

View File

@ -121,7 +121,7 @@ async fn test_success() {
usdc_reserve.liquidity_supply,
sol_reserve.pubkey,
sol_reserve.collateral_supply,
obligation.keypair.pubkey(),
obligation.pubkey,
obligation.token_mint,
obligation.token_account,
lending_market.pubkey,