solana
rust
Solana's lending protocol is inspired by EVM AAVE & Compound.
In this article, we will go through the basic idea of spl token-lending protocol to get to know how it work in very high level concept.
There are 3 very important PDA account categories in token-lending protocol
Everyone is allowed to call the lending program to create their own lending market to start they own business. the lending market represets a whole "market place" such as Larix or Soland in Solana
A Reserve is the pool (or platform) in which users can operate the lend (supply/deposit) or borrow against certain token for example USDC token.
We can have as many Reserves in a lending market as we want.
The more the Reserve a lending market has, the more lending options which allows users to perform the operation.
A Reserve account is made of two main sections (accounts)
// /src/state/reserve.rs
...
/// Lending market reserve state
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Reserve {
/// Version of the struct
pub version: u8,
/// Last slot when supply and rates updated
pub last_update: LastUpdate,
/// Lending market address
pub lending_market: Pubkey,
/// Reserve liquidity
pub liquidity: ReserveLiquidity,
/// Reserve collateral
pub collateral: ReserveCollateral,
/// Reserve configuration values
pub config: ReserveConfig,
}
...
This liquidity means the "LIQUIDITY" in certian target token (ex. USDC) allows user to perform lending behavior. It contains those information of target token.
There are two very important roles in Liquidty.
...
/// Reserve liquidity
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ReserveLiquidity {
/// Reserve liquidity mint address
pub mint_pubkey: Pubkey,
/// Reserve liquidity mint decimals
pub mint_decimals: u8,
/// Reserve liquidity supply address
pub supply_pubkey: Pubkey,
/// Reserve liquidity fee receiver address
pub fee_receiver: Pubkey,
/// Reserve liquidity oracle account
pub oracle_pubkey: Pubkey,
/// Reserve liquidity available
pub available_amount: u64,
/// Reserve liquidity borrowed
pub borrowed_amount_wads: Decimal,
/// Reserve liquidity cumulative borrow rate
pub cumulative_borrow_rate_wads: Decimal,
/// Reserve liquidity market price in quote currency
pub market_price: Decimal,
}
...
Once the user performce any certain lending feature in the Lending Market. The program creates the corresponding "Obligation" account for his/her wallet.
It records all the important records in the following two fields.
// /src/state/obligation.rs
...
/// Lending market obligation state
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Obligation {
/// Version of the struct
pub version: u8,
/// Last update to collateral, liquidity, or their market values
pub last_update: LastUpdate,
/// Lending market address
pub lending_market: Pubkey,
/// Owner authority which can borrow liquidity
pub owner: Pubkey,
/// Deposited collateral for the obligation, unique by deposit reserve address
pub deposits: Vec<ObligationCollateral>,
/// Borrowed liquidity for the obligation, unique by borrow reserve address
pub borrows: Vec<ObligationLiquidity>,
/// Market value of deposits
pub deposited_value: Decimal,
/// Market value of borrows
pub borrowed_value: Decimal,
/// The maximum borrow value at the weighted average loan to value ratio
pub allowed_borrow_value: Decimal,
/// The dangerous borrow value at the weighted average liquidation threshold
pub unhealthy_borrow_value: Decimal,
}
...
Now we have some basic concpet about how does the token-lending protocol look like.
Now, lets see how it works in the background when the users perform the lend or borrow. Or even their collateral gets liquidated.
The instructions related to lend (suply) are #4 & #8
Lets see the args of instructions below:
// /src/instructions.rs
...
// 4
/// Deposit liquidity into a reserve in exchange for collateral. Collateral represents a share
/// of the reserve liquidity pool.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` Source liquidity token account.
/// $authority can transfer $liquidity_amount.
/// 1. `[writable]` Destination collateral token account.
/// 2. `[writable]` Reserve account.
/// 3. `[writable]` Reserve liquidity supply SPL Token account.
/// 4. `[writable]` Reserve collateral SPL Token mint.
/// 5. `[]` Lending market account.
/// 6. `[]` Derived lending market authority.
/// 7. `[signer]` User transfer authority ($authority).
/// 8. `[]` Clock sysvar.
/// 9. `[]` Token program id.
DepositReserveLiquidity {
/// Amount of liquidity to deposit in exchange for collateral tokens
liquidity_amount: u64,
},
...
// 8
/// Deposit collateral to an obligation. Requires a refreshed reserve.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` Source collateral token account.
/// Minted by deposit reserve collateral mint.
/// $authority can transfer $collateral_amount.
/// 1. `[writable]` Destination deposit reserve collateral supply SPL Token account.
/// 2. `[]` Deposit reserve account - refreshed.
/// 3. `[writable]` Obligation account.
/// 4. `[]` Lending market account.
/// 5. `[signer]` Obligation owner.
/// 6. `[signer]` User transfer authority ($authority).
/// 7. `[]` Clock sysvar.
/// 8. `[]` Token program id.
DepositObligationCollateral {
/// Amount of collateral tokens to deposit
collateral_amount: u64,
},
Then we will go through the proccess of these two instruction to see how it works
// /src/processor.rs
...
#[inline(never)] // avoid stack frame limit
fn process_deposit_obligation_collateral(
program_id: &Pubkey,
collateral_amount: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if collateral_amount == 0 {
msg!("Collateral amount provided cannot be zero");
return Err(LendingError::InvalidAmount.into());
}
let account_info_iter = &mut accounts.iter();
let source_collateral_info = next_account_info(account_info_iter)?;
let destination_collateral_info = next_account_info(account_info_iter)?;
let deposit_reserve_info = next_account_info(account_info_iter)?;
let obligation_info = next_account_info(account_info_iter)?;
let lending_market_info = next_account_info(account_info_iter)?;
let obligation_owner_info = next_account_info(account_info_iter)?;
let user_transfer_authority_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?;
let token_program_id = next_account_info(account_info_iter)?;
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
if lending_market_info.owner != program_id {
msg!("Lending market provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &lending_market.token_program_id != token_program_id.key {
msg!("Lending market token program does not match the token program provided");
return Err(LendingError::InvalidTokenProgram.into());
}
let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?;
if deposit_reserve_info.owner != program_id {
msg!("Deposit reserve provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &deposit_reserve.lending_market != lending_market_info.key {
msg!("Deposit reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key {
msg!("Deposit reserve collateral supply cannot be used as the source collateral provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key {
msg!(
"Deposit reserve collateral supply must be used as the destination collateral provided"
);
return Err(LendingError::InvalidAccountInput.into());
}
if deposit_reserve.last_update.is_stale(clock.slot)? {
msg!("Deposit reserve is stale and must be refreshed in the current slot");
return Err(LendingError::ReserveStale.into());
}
if deposit_reserve.config.loan_to_value_ratio == 0 {
msg!("Deposit reserve has collateral disabled for borrowing");
return Err(LendingError::ReserveCollateralDisabled.into());
}
let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?;
if obligation_info.owner != program_id {
msg!("Obligation provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &obligation.lending_market != lending_market_info.key {
msg!("Obligation lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &obligation.owner != obligation_owner_info.key {
msg!("Obligation owner does not match the obligation owner provided");
return Err(LendingError::InvalidObligationOwner.into());
}
if !obligation_owner_info.is_signer {
msg!("Obligation owner provided must be a signer");
return Err(LendingError::InvalidSigner.into());
}
obligation
.find_or_add_collateral_to_deposits(*deposit_reserve_info.key)?
.deposit(collateral_amount)?;
obligation.last_update.mark_stale();
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
spl_token_transfer(TokenTransferParams {
source: source_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(),
})?;
Ok(())
}
...
After verifing all the accounts, obligation .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? .deposit(collateral_amount)?;
is called to update the deposits info inside the user's obligation.
Then spl_token_transfer
is called to transfer tokens from the user's token account to the vault account in reserve's liquidity.
#[inline(never)] // avoid stack frame limit
fn process_deposit_obligation_collateral(
program_id: &Pubkey,
collateral_amount: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if collateral_amount == 0 {
msg!("Collateral amount provided cannot be zero");
return Err(LendingError::InvalidAmount.into());
}
let account_info_iter = &mut accounts.iter();
let source_collateral_info = next_account_info(account_info_iter)?;
let destination_collateral_info = next_account_info(account_info_iter)?;
let deposit_reserve_info = next_account_info(account_info_iter)?;
let obligation_info = next_account_info(account_info_iter)?;
let lending_market_info = next_account_info(account_info_iter)?;
let obligation_owner_info = next_account_info(account_info_iter)?;
let user_transfer_authority_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?;
let token_program_id = next_account_info(account_info_iter)?;
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
if lending_market_info.owner != program_id {
msg!("Lending market provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &lending_market.token_program_id != token_program_id.key {
msg!("Lending market token program does not match the token program provided");
return Err(LendingError::InvalidTokenProgram.into());
}
let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?;
if deposit_reserve_info.owner != program_id {
msg!("Deposit reserve provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &deposit_reserve.lending_market != lending_market_info.key {
msg!("Deposit reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key {
msg!("Deposit reserve collateral supply cannot be used as the source collateral provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key {
msg!(
"Deposit reserve collateral supply must be used as the destination collateral provided"
);
return Err(LendingError::InvalidAccountInput.into());
}
if deposit_reserve.last_update.is_stale(clock.slot)? {
msg!("Deposit reserve is stale and must be refreshed in the current slot");
return Err(LendingError::ReserveStale.into());
}
if deposit_reserve.config.loan_to_value_ratio == 0 {
msg!("Deposit reserve has collateral disabled for borrowing");
return Err(LendingError::ReserveCollateralDisabled.into());
}
let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?;
if obligation_info.owner != program_id {
msg!("Obligation provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &obligation.lending_market != lending_market_info.key {
msg!("Obligation lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &obligation.owner != obligation_owner_info.key {
msg!("Obligation owner does not match the obligation owner provided");
return Err(LendingError::InvalidObligationOwner.into());
}
if !obligation_owner_info.is_signer {
msg!("Obligation owner provided must be a signer");
return Err(LendingError::InvalidSigner.into());
}
obligation
.find_or_add_collateral_to_deposits(*deposit_reserve_info.key)?
.deposit(collateral_amount)?;
obligation.last_update.mark_stale();
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
spl_token_transfer(TokenTransferParams {
source: source_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(),
})?;
Ok(())
}
We will be able to see that we make use of the method of obligation
to find the existing deposits target and update the data, then mint the respective cToken to that user
The instructions related to withdraw are #4 & #8
Lets see the args of instructions below:
#5: RedeemReserveCollateral
#9: WithdrawObligationColletral
// src/instruction.rs
...
// 5
/// Redeem collateral from a reserve in exchange for liquidity.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` Source collateral token account.
/// $authority can transfer $collateral_amount.
/// 1. `[writable]` Destination liquidity token account.
/// 2. `[writable]` Reserve account.
/// 3. `[writable]` Reserve collateral SPL Token mint.
/// 4. `[writable]` Reserve liquidity supply SPL Token account.
/// 5. `[]` Lending market account.
/// 6. `[]` Derived lending market authority.
/// 7. `[signer]` User transfer authority ($authority).
/// 8. `[]` Clock sysvar.
/// 9. `[]` Token program id.
RedeemReserveCollateral {
/// Amount of collateral tokens to redeem in exchange for liquidity
collateral_amount: u64,
},
...
// 9
/// Withdraw collateral from an obligation. Requires a refreshed obligation and reserve.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` Source withdraw reserve collateral supply SPL Token account.
/// 1. `[writable]` Destination collateral token account.
/// Minted by withdraw reserve collateral mint.
/// 2. `[]` Withdraw reserve account - refreshed.
/// 3. `[writable]` Obligation account - refreshed.
/// 4. `[]` Lending market account.
/// 5. `[]` Derived lending market authority.
/// 6. `[signer]` Obligation owner.
/// 7. `[]` Clock sysvar.
/// 8. `[]` Token program id.
WithdrawObligationCollateral {
/// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount
collateral_amount: u64,
},
fn process_redeem_reserve_collateral(
program_id: &Pubkey,
collateral_amount: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if collateral_amount == 0 {
msg!("Collateral amount provided cannot be zero");
return Err(LendingError::InvalidAmount.into());
}
let account_info_iter = &mut accounts.iter();
let source_collateral_info = next_account_info(account_info_iter)?;
let destination_liquidity_info = next_account_info(account_info_iter)?;
let reserve_info = next_account_info(account_info_iter)?;
let reserve_collateral_mint_info = next_account_info(account_info_iter)?;
let reserve_liquidity_supply_info = next_account_info(account_info_iter)?;
let lending_market_info = next_account_info(account_info_iter)?;
let lending_market_authority_info = next_account_info(account_info_iter)?;
let user_transfer_authority_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?;
let token_program_id = next_account_info(account_info_iter)?;
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
if lending_market_info.owner != program_id {
msg!("Lending market provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &lending_market.token_program_id != token_program_id.key {
msg!("Lending market token program does not match the token program provided");
return Err(LendingError::InvalidTokenProgram.into());
}
let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?;
if reserve_info.owner != program_id {
msg!("Reserve provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &reserve.lending_market != lending_market_info.key {
msg!("Reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &reserve.collateral.mint_pubkey != reserve_collateral_mint_info.key {
msg!("Reserve collateral mint does not match the reserve collateral mint provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &reserve.collateral.supply_pubkey == source_collateral_info.key {
msg!("Reserve collateral supply cannot be used as the source collateral provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &reserve.liquidity.supply_pubkey != reserve_liquidity_supply_info.key {
msg!("Reserve liquidity supply does not match the reserve liquidity supply provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key {
msg!("Reserve liquidity supply cannot be used as the destination liquidity provided");
return Err(LendingError::InvalidAccountInput.into());
}
if reserve.last_update.is_stale(clock.slot)? {
msg!("Reserve is stale and must be refreshed in the current slot");
return Err(LendingError::ReserveStale.into());
}
let authority_signer_seeds = &[
lending_market_info.key.as_ref(),
&[lending_market.bump_seed],
];
let lending_market_authority_pubkey =
Pubkey::create_program_address(authority_signer_seeds, program_id)?;
if &lending_market_authority_pubkey != lending_market_authority_info.key {
msg!(
"Derived lending market authority does not match the lending market authority provided"
);
return Err(LendingError::InvalidMarketAuthority.into());
}
let liquidity_amount = reserve.redeem_collateral(collateral_amount)?;
reserve.last_update.mark_stale();
Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;
spl_token_burn(TokenBurnParams {
mint: reserve_collateral_mint_info.clone(),
source: source_collateral_info.clone(),
amount: collateral_amount,
authority: user_transfer_authority_info.clone(),
authority_signer_seeds: &[],
token_program: token_program_id.clone(),
})?;
spl_token_transfer(TokenTransferParams {
source: reserve_liquidity_supply_info.clone(),
destination: destination_liquidity_info.clone(),
amount: liquidity_amount,
authority: lending_market_authority_info.clone(),
authority_signer_seeds,
token_program: token_program_id.clone(),
})?;
Ok(())
}
As can be seen in the last section, we make use of redeem_collateral
method of serserve
instance to get the amount of target token to withdraw by passing into the amount of cToken.
And then burn out those cToken and transfer back the target token to the user's wallet.
#[inline(never)] // avoid stack frame limit
fn process_withdraw_obligation_collateral(
program_id: &Pubkey,
collateral_amount: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if collateral_amount == 0 {
msg!("Collateral amount provided cannot be zero");
return Err(LendingError::InvalidAmount.into());
}
let account_info_iter = &mut accounts.iter();
let source_collateral_info = next_account_info(account_info_iter)?;
let destination_collateral_info = next_account_info(account_info_iter)?;
let withdraw_reserve_info = next_account_info(account_info_iter)?;
let obligation_info = next_account_info(account_info_iter)?;
let lending_market_info = next_account_info(account_info_iter)?;
let lending_market_authority_info = next_account_info(account_info_iter)?;
let obligation_owner_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?;
let token_program_id = next_account_info(account_info_iter)?;
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
if lending_market_info.owner != program_id {
msg!("Lending market provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &lending_market.token_program_id != token_program_id.key {
msg!("Lending market token program does not match the token program provided");
return Err(LendingError::InvalidTokenProgram.into());
}
let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?;
if withdraw_reserve_info.owner != program_id {
msg!("Withdraw reserve provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &withdraw_reserve.lending_market != lending_market_info.key {
msg!("Withdraw reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key {
msg!("Withdraw reserve collateral supply must be used as the source collateral provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key {
msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided");
return Err(LendingError::InvalidAccountInput.into());
}
if withdraw_reserve.last_update.is_stale(clock.slot)? {
msg!("Withdraw reserve is stale and must be refreshed in the current slot");
return Err(LendingError::ReserveStale.into());
}
let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?;
if obligation_info.owner != program_id {
msg!("Obligation provided is not owned by the lending program");
return Err(LendingError::InvalidAccountOwner.into());
}
if &obligation.lending_market != lending_market_info.key {
msg!("Obligation lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
if &obligation.owner != obligation_owner_info.key {
msg!("Obligation owner does not match the obligation owner provided");
return Err(LendingError::InvalidObligationOwner.into());
}
if !obligation_owner_info.is_signer {
msg!("Obligation owner provided must be a signer");
return Err(LendingError::InvalidSigner.into());
}
if obligation.last_update.is_stale(clock.slot)? {
msg!("Obligation is stale and must be refreshed in the current slot");
return Err(LendingError::ObligationStale.into());
}
let (collateral, collateral_index) =
obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?;
if collateral.deposited_amount == 0 {
msg!("Collateral deposited amount is zero");
return Err(LendingError::ObligationCollateralEmpty.into());
}
let authority_signer_seeds = &[
lending_market_info.key.as_ref(),
&[lending_market.bump_seed],
];
let lending_market_authority_pubkey =
Pubkey::create_program_address(authority_signer_seeds, program_id)?;
if &lending_market_authority_pubkey != lending_market_authority_info.key {
msg!(
"Derived lending market authority does not match the lending market authority provided"
);
return Err(LendingError::InvalidMarketAuthority.into());
}
let withdraw_amount = if obligation.borrows.is_empty() {
if collateral_amount == u64::MAX {
collateral.deposited_amount
} else {
collateral.deposited_amount.min(collateral_amount)
}
} else if obligation.deposited_value == Decimal::zero() {
msg!("Obligation deposited value is zero");
return Err(LendingError::ObligationDepositsZero.into());
} else {
let max_withdraw_value = obligation.max_withdraw_value()?;
if max_withdraw_value == Decimal::zero() {
msg!("Maximum withdraw value is zero");
return Err(LendingError::WithdrawTooLarge.into());
}
let withdraw_amount = if collateral_amount == u64::MAX {
let withdraw_value = max_withdraw_value.min(collateral.market_value);
let withdraw_pct = withdraw_value.try_div(collateral.market_value)?;
withdraw_pct
.try_mul(collateral.deposited_amount)?
.try_floor_u64()?
.min(collateral.deposited_amount)
} else {
let withdraw_amount = collateral_amount.min(collateral.deposited_amount);
let withdraw_pct =
Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?;
let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?;
if withdraw_value > max_withdraw_value {
msg!("Withdraw value cannot exceed maximum withdraw value");
return Err(LendingError::WithdrawTooLarge.into());
}
withdraw_amount
};
if withdraw_amount == 0 {
msg!("Withdraw amount is too small to transfer collateral");
return Err(LendingError::WithdrawTooSmall.into());
}
withdraw_amount
};
obligation.withdraw(withdraw_amount, collateral_index)?;
obligation.last_update.mark_stale();
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
spl_token_transfer(TokenTransferParams {
source: source_collateral_info.clone(),
destination: destination_collateral_info.clone(),
amount: withdraw_amount,
authority: lending_market_authority_info.clone(),
authority_signer_seeds,
token_program: token_program_id.clone(),
})?;
Ok(())
}
In this section, we transfer back the cToken from the supply vault of the reserve back to user. Then as been explain in previous section, burn it out.