token lending: DepositObligationCollateral & WithdrawObligationCollateral instructions (#1229)

This commit is contained in:
Jordan Sexton 2021-03-06 11:02:41 -06:00 committed by GitHub
parent 8fd6f8ec55
commit 27c94293ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 965 additions and 6 deletions

View File

@ -125,6 +125,17 @@ pub enum LendingError {
/// Token burn failed
#[error("Token burn failed")]
TokenBurnFailed,
// 35
/// 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,
/// Obligation collateral cannot be withdrawn below required amount
#[error("Obligation collateral cannot be withdrawn below required amount")]
ObligationCollateralWithdrawBelowRequired,
}
impl From<LendingError> for ProgramError {

View File

@ -197,6 +197,47 @@ pub enum LendingInstruction {
/// 1. `[writable]` Reserve account.
/// .. `[writable]` Additional reserve accounts.
AccrueReserveInterest,
/// Deposit additional collateral to an obligation.
///
/// 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 token mint
/// 5. `[writable]` Obligation token output
/// 6. `[]` Lending market account.
/// 7. `[]` Derived lending market authority.
/// 8. `[]` User transfer authority ($authority).
/// 9. '[]` Token program id
DepositObligationCollateral {
/// Amount of collateral to deposit
collateral_amount: u64,
},
/// Withdraw excess collateral from an obligation. The loan must remain healthy.
///
/// 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. `[]` Borrow reserve account.
/// 4. `[writable]` Obligation
/// 5. `[writable]` Obligation token mint
/// 6. `[writable]` Obligation token input
/// 7. `[]` Lending market account.
/// 8. `[]` Derived lending market authority.
/// 9. `[]` User transfer authority ($authority).
/// 10 `[]` Dex market
/// 11 `[]` Dex market order book side
/// 12 `[]` Temporary memory
/// 13 `[]` Clock sysvar
/// 14 '[]` Token program id
WithdrawObligationCollateral {
/// Amount of collateral to withdraw
collateral_amount: u64,
},
}
impl LendingInstruction {
@ -266,6 +307,14 @@ impl LendingInstruction {
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 }
}
_ => return Err(LendingError::InstructionUnpackError.into()),
})
}
@ -376,6 +425,14 @@ impl LendingInstruction {
Self::AccrueReserveInterest => {
buf.push(8);
}
Self::DepositObligationCollateral { collateral_amount } => {
buf.push(9);
buf.extend_from_slice(&collateral_amount.to_le_bytes());
}
Self::WithdrawObligationCollateral { collateral_amount } => {
buf.push(10);
buf.extend_from_slice(&collateral_amount.to_le_bytes());
}
}
buf
}
@ -712,3 +769,78 @@ pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec<Pubkey>)
data: LendingInstruction::AccrueReserveInterest.pack(),
}
}
/// Creates a 'DepositObligationCollateral' instruction.
#[allow(clippy::too_many_arguments)]
pub fn deposit_obligation_collateral(
program_id: Pubkey,
collateral_amount: u64,
source_collateral_pubkey: Pubkey,
destination_collateral_pubkey: Pubkey,
deposit_reserve_pubkey: Pubkey,
obligation_pubkey: Pubkey,
obligation_mint_pubkey: Pubkey,
obligation_output_pubkey: Pubkey,
lending_market_pubkey: Pubkey,
lending_market_authority_pubkey: Pubkey,
user_transfer_authority_pubkey: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(source_collateral_pubkey, false),
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_readonly(lending_market_pubkey, false),
AccountMeta::new_readonly(lending_market_authority_pubkey, false),
AccountMeta::new_readonly(user_transfer_authority_pubkey, true),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(),
}
}
/// Creates an 'WithdrawObligationCollateral' instruction.
#[allow(clippy::too_many_arguments)]
pub fn withdraw_obligation_collateral(
program_id: Pubkey,
collateral_amount: u64,
source_collateral_pubkey: Pubkey,
destination_collateral_pubkey: Pubkey,
withdraw_reserve_pubkey: Pubkey,
borrow_reserve_pubkey: Pubkey,
obligation_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,
accounts: vec![
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_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),
],
data: LendingInstruction::WithdrawObligationCollateral { collateral_amount }.pack(),
}
}

View File

@ -76,6 +76,14 @@ pub fn process_instruction(
msg!("Instruction: Accrue Interest");
process_accrue_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 } => {
msg!("Instruction: Withdraw Obligation Collateral");
process_withdraw_obligation_collateral(program_id, collateral_amount, accounts)
}
}
}
@ -1178,6 +1186,317 @@ fn process_accrue_interest(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro
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 {
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 user_transfer_authority_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 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());
}
if &deposit_reserve.collateral.supply_pubkey != destination_collateral_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 collateral 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.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());
}
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());
}
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 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(),
destination: destination_collateral_info.clone(),
amount: collateral_amount,
authority: user_transfer_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: 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(
program_id: &Pubkey,
collateral_amount: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if collateral_amount == 0 {
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 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_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());
}
if &lending_market.token_program_id != token_program_id.key {
return Err(LendingError::InvalidTokenProgram.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 != 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 != 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"
);
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());
}
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 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());
}
}
let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?;
if obligation_info.owner != program_id {
return Err(LendingError::InvalidAccountOwner.into());
}
if &obligation.collateral_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 {
msg!("Obligation token mint input doesn't match existing obligation token mint");
return Err(LendingError::InvalidTokenMint.into());
}
let obligation_token_input = Token::unpack(&obligation_token_input_info.data.borrow())?;
if obligation_token_input_info.owner != token_program_id.key {
return Err(LendingError::InvalidTokenOwner.into());
}
if &obligation_token_input.mint != obligation_token_mint_info.key {
return Err(LendingError::InvalidTokenMint.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)?;
let obligation_collateral_amount = obligation.deposited_collateral_tokens;
if obligation_collateral_amount == 0 {
return Err(LendingError::ObligationEmpty.into());
}
if obligation_collateral_amount < collateral_amount {
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,
trade_simulator,
)?;
if obligation_collateral_amount < required_collateral {
return Err(LendingError::ObligationCollateralBelowRequired.into());
}
let remaining_collateral = obligation_collateral_amount
.checked_sub(collateral_amount)
.ok_or(LendingError::MathOverflow)?;
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())?;
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(),
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(),
})?;
// withdraw collateral
spl_token_transfer(TokenTransferParams {
source: source_collateral_info.clone(),
destination: destination_collateral_info.clone(),
amount: collateral_amount,
authority: lending_market_authority_info.clone(),
authority_signer_seeds,
token_program: token_program_id.clone(),
})?;
Ok(())
}
fn 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());

View File

@ -83,6 +83,18 @@ impl Obligation {
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<u64, ProgramError> {
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()
}
/// Accrue interest
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
@ -130,12 +142,10 @@ impl Obligation {
withdraw_amount.try_floor_u64()?
};
let obligation_token_amount = {
let withdraw_pct = Decimal::from(collateral_withdraw_amount)
.try_div(self.deposited_collateral_tokens)?;
let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?;
token_amount.try_floor_u64()?
};
let obligation_token_amount = self.collateral_to_obligation_token_amount(
collateral_withdraw_amount,
obligation_token_supply,
)?;
self.borrowed_liquidity_wads =
self.borrowed_liquidity_wads.try_sub(decimal_repay_amount)?;

View File

@ -0,0 +1,151 @@
#![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::deposit_obligation_collateral, 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_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(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;
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 usdc_mint = add_usdc_mint(&mut test);
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
let sol_reserve = add_reserve(
&mut test,
&user_accounts_owner,
&lending_market,
AddReserveArgs {
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
liquidity_mint_decimals: 9,
liquidity_mint_pubkey: spl_token::native_mint::id(),
collateral_amount: OBLIGATION_COLLATERAL,
config: TEST_RESERVE_CONFIG,
..AddReserveArgs::default()
},
);
let usdc_reserve = add_reserve(
&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),
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
let initial_collateral_supply_balance =
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).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;
let mut transaction = Transaction::new_with_payer(
&[
approve(
&spl_token::id(),
&sol_reserve.user_collateral_account,
&user_transfer_authority.pubkey(),
&user_accounts_owner.pubkey(),
&[],
DEPOSIT_COLLATERAL,
)
.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,
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());
// check that collateral tokens were transferred
let collateral_supply_balance =
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
assert_eq!(
collateral_supply_balance,
initial_collateral_supply_balance + DEPOSIT_COLLATERAL
);
let user_collateral_balance =
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await;
assert_eq!(
user_collateral_balance,
initial_user_collateral_balance - DEPOSIT_COLLATERAL
);
// 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
);
}

View File

@ -0,0 +1,336 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError,
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction::create_account,
transaction::{Transaction, TransactionError},
};
use helpers::*;
use spl_token::instruction::approve;
use spl_token_lending::{
error::LendingError,
instruction::withdraw_obligation_collateral,
math::Decimal,
processor::process_instruction,
state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR},
};
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() {
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(84_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;
// from Reserve::required_collateral_for_borrow
const REQUIRED_COLLATERAL: u64 = 45_929_968_168;
const WITHDRAW_COLLATERAL: u64 = OBLIGATION_COLLATERAL - REQUIRED_COLLATERAL;
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(
&mut test,
&user_accounts_owner,
&lending_market,
AddReserveArgs {
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,
config: TEST_RESERVE_CONFIG,
..AddReserveArgs::default()
},
);
let usdc_reserve = add_reserve(
&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),
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
let initial_collateral_supply_balance =
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).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;
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,
&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(),
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,
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,
],
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;
assert_eq!(
collateral_supply_balance,
initial_collateral_supply_balance - WITHDRAW_COLLATERAL
);
let user_collateral_balance =
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await;
assert_eq!(
user_collateral_balance,
initial_user_collateral_balance + WITHDRAW_COLLATERAL
);
// check that obligation tokens were burned
let obligation_token_balance =
get_token_balance(&mut banks_client, obligation.token_account).await;
assert_eq!(
obligation_token_balance,
initial_obligation_token_balance - WITHDRAW_COLLATERAL
);
}
#[tokio::test]
async fn test_withdraw_below_required() {
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(84_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;
// from Reserve::required_collateral_for_borrow
const REQUIRED_COLLATERAL: u64 = 45_929_968_168;
const WITHDRAW_COLLATERAL: u64 = OBLIGATION_COLLATERAL - REQUIRED_COLLATERAL + 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(
&mut test,
&user_accounts_owner,
&lending_market,
AddReserveArgs {
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,
config: TEST_RESERVE_CONFIG,
..AddReserveArgs::default()
},
);
let usdc_reserve = add_reserve(
&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),
},
);
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,
&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(),
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,
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,
],
recent_blockhash,
);
// check that transaction fails
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
3,
InstructionError::Custom(
LendingError::ObligationCollateralWithdrawBelowRequired as u32
)
)
);
}