token lending: DepositObligationCollateral & WithdrawObligationCollateral instructions (#1229)
This commit is contained in:
parent
8fd6f8ec55
commit
27c94293ce
|
@ -125,6 +125,17 @@ pub enum LendingError {
|
||||||
/// Token burn failed
|
/// Token burn failed
|
||||||
#[error("Token burn failed")]
|
#[error("Token burn failed")]
|
||||||
TokenBurnFailed,
|
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 {
|
impl From<LendingError> for ProgramError {
|
||||||
|
|
|
@ -197,6 +197,47 @@ pub enum LendingInstruction {
|
||||||
/// 1. `[writable]` Reserve account.
|
/// 1. `[writable]` Reserve account.
|
||||||
/// .. `[writable]` Additional reserve accounts.
|
/// .. `[writable]` Additional reserve accounts.
|
||||||
AccrueReserveInterest,
|
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 {
|
impl LendingInstruction {
|
||||||
|
@ -266,6 +307,14 @@ impl LendingInstruction {
|
||||||
Self::LiquidateObligation { liquidity_amount }
|
Self::LiquidateObligation { liquidity_amount }
|
||||||
}
|
}
|
||||||
8 => Self::AccrueReserveInterest,
|
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()),
|
_ => return Err(LendingError::InstructionUnpackError.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -376,6 +425,14 @@ impl LendingInstruction {
|
||||||
Self::AccrueReserveInterest => {
|
Self::AccrueReserveInterest => {
|
||||||
buf.push(8);
|
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
|
buf
|
||||||
}
|
}
|
||||||
|
@ -712,3 +769,78 @@ pub fn accrue_reserve_interest(program_id: Pubkey, reserve_pubkeys: Vec<Pubkey>)
|
||||||
data: LendingInstruction::AccrueReserveInterest.pack(),
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,14 @@ pub fn process_instruction(
|
||||||
msg!("Instruction: Accrue Interest");
|
msg!("Instruction: Accrue Interest");
|
||||||
process_accrue_interest(program_id, accounts)
|
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(())
|
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 {
|
fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
|
||||||
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
|
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
|
||||||
msg!(&rent.minimum_balance(account_info.data_len()).to_string());
|
msg!(&rent.minimum_balance(account_info.data_len()).to_string());
|
||||||
|
|
|
@ -83,6 +83,18 @@ impl Obligation {
|
||||||
loan.try_div(collateral_value)
|
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
|
/// Accrue interest
|
||||||
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
|
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
|
||||||
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
|
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
|
||||||
|
@ -130,12 +142,10 @@ impl Obligation {
|
||||||
withdraw_amount.try_floor_u64()?
|
withdraw_amount.try_floor_u64()?
|
||||||
};
|
};
|
||||||
|
|
||||||
let obligation_token_amount = {
|
let obligation_token_amount = self.collateral_to_obligation_token_amount(
|
||||||
let withdraw_pct = Decimal::from(collateral_withdraw_amount)
|
collateral_withdraw_amount,
|
||||||
.try_div(self.deposited_collateral_tokens)?;
|
obligation_token_supply,
|
||||||
let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?;
|
)?;
|
||||||
token_amount.try_floor_u64()?
|
|
||||||
};
|
|
||||||
|
|
||||||
self.borrowed_liquidity_wads =
|
self.borrowed_liquidity_wads =
|
||||||
self.borrowed_liquidity_wads.try_sub(decimal_repay_amount)?;
|
self.borrowed_liquidity_wads.try_sub(decimal_repay_amount)?;
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue