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
|
||||
#[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 {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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