Overview
Liquidations occur when a borrower’s health factor falls below 1.0 (unhealthy LTV). Liquidators repay a portion of the debt and receive collateral at a discount (liquidation bonus).Key Concepts
- Unhealthy Obligation: LTV ratio exceeds maximum threshold
- Liquidation Bonus: Discount given to liquidators (typically 5-10%)
- Partial Liquidation: Liquidate up to a certain percentage per transaction
- Close Factor: Maximum percentage of debt that can be repaid in one liquidation
When Positions Become Liquidatable
An obligation becomes liquidatable when:// Health factor calculation
function isLiquidatable(obligation: Obligation, reserves: Reserve[]): boolean {
let borrowedValue = 0;
let allowedBorrowValue = 0;
// Calculate total borrowed value
for (const borrow of obligation.borrows) {
if (borrow.borrowedAmountSf.gt(new BN(0))) {
const reserve = reserves.find(r => r.pubkey.equals(borrow.borrowReserve));
borrowedValue += borrow.borrowedAmountSf.toNumber() * reserve.liquidity.marketPrice.toNumber();
}
}
// Calculate allowed borrow value based on collateral
for (const deposit of obligation.deposits) {
if (deposit.depositedAmount.gt(new BN(0))) {
const reserve = reserves.find(r => r.pubkey.equals(deposit.depositReserve));
const collateralValue = deposit.depositedAmount.toNumber() * reserve.collateral.marketPrice.toNumber();
// Apply LTV ratio
allowedBorrowValue += collateralValue * (reserve.config.liquidationThreshold / 100);
}
}
// Liquidatable if borrowed value exceeds allowed value
return borrowedValue > allowedBorrowValue;
}
Liquidate Obligation
Instruction: liquidate_obligation_and_redeem_reserve_collateral_v2
fn process_impl(
accounts: &LiquidateObligationAndRedeemReserveCollateral,
remaining_accounts: &[AccountInfo],
liquidity_amount: u64,
min_acceptable_received_liquidity_amount: u64,
max_allowed_ltv_override_percent: u64,
) -> Result<()> {
lending_checks::liquidate_obligation_checks(accounts)?;
lending_checks::redeem_reserve_collateral_checks(/* ... */)?;
let lending_market = &accounts.lending_market.load()?;
let obligation = &mut accounts.obligation.load_mut()?;
let clock = &Clock::get()?;
// LTV override only for self-liquidation in staging
let max_allowed_ltv_override_pct_opt =
if accounts.liquidator.key() == obligation.owner && max_allowed_ltv_override_percent > 0 {
if cfg!(feature = "staging") {
Some(max_allowed_ltv_override_percent)
} else {
None
}
} else {
None
};
// Get all deposit reserves for health calculation
let deposit_reserves = remaining_accounts.iter().map(|a| {
FatAccountLoader::try_from(a).expect("Invalid deposit reserve")
});
// Calculate liquidation amounts
let LiquidateAndRedeemResult {
repay_amount,
withdraw_collateral_amount,
withdraw_amount,
total_withdraw_liquidity_amount,
..
} = lending_operations::liquidate_and_redeem(
lending_market,
&accounts.repay_reserve,
&accounts.withdraw_reserve,
obligation,
clock,
liquidity_amount,
min_acceptable_received_liquidity_amount,
max_allowed_ltv_override_pct_opt,
deposit_reserves,
)?;
// Transfer repayment from liquidator to reserve
token_transfer::repay_obligation_liquidity_transfer(
accounts.repay_liquidity_token_program.to_account_info(),
accounts.repay_reserve_liquidity_mint.to_account_info(),
accounts.user_source_liquidity.to_account_info(),
accounts.repay_reserve_liquidity_supply.to_account_info(),
accounts.liquidator.to_account_info(),
repay_amount,
accounts.repay_reserve_liquidity_mint.decimals,
)?;
let lending_market_key = accounts.lending_market.key();
let authority_signer_seeds = gen_signer_seeds!(
lending_market_key,
lending_market.bump_seed as u8
);
// Transfer collateral from reserve to liquidator
token_transfer::withdraw_obligation_collateral_transfer(
accounts.collateral_token_program.to_account_info(),
accounts.user_destination_collateral.to_account_info(),
accounts.withdraw_reserve_collateral_supply.to_account_info(),
accounts.lending_market_authority.to_account_info(),
authority_signer_seeds,
withdraw_amount,
)?;
// Redeem collateral to liquidity if requested
if let Some((withdraw_liquidity_amount, protocol_fee)) = total_withdraw_liquidity_amount {
token_transfer::redeem_reserve_collateral_transfer(
accounts.collateral_token_program.to_account_info(),
accounts.withdraw_liquidity_token_program.to_account_info(),
accounts.withdraw_reserve_liquidity_mint.to_account_info(),
accounts.withdraw_reserve_collateral_mint.to_account_info(),
accounts.user_destination_collateral.to_account_info(),
accounts.liquidator.to_account_info(),
accounts.withdraw_reserve_liquidity_supply.to_account_info(),
accounts.user_destination_liquidity.to_account_info(),
accounts.lending_market_authority.to_account_info(),
authority_signer_seeds,
withdraw_collateral_amount,
withdraw_liquidity_amount,
accounts.withdraw_reserve_liquidity_mint.decimals,
)?;
// Transfer protocol fee
token_interface::transfer_checked(
CpiContext::new(
accounts.withdraw_liquidity_token_program.to_account_info(),
token_interface::TransferChecked {
from: accounts.user_destination_liquidity.to_account_info(),
to: accounts.withdraw_reserve_liquidity_fee_receiver.to_account_info(),
authority: accounts.liquidator.to_account_info(),
mint: accounts.withdraw_reserve_liquidity_mint.to_account_info(),
},
),
protocol_fee,
accounts.withdraw_reserve_liquidity_mint.decimals,
)?;
}
Ok(())
}
Account Context
#[derive(Accounts)]
pub struct LiquidateObligationAndRedeemReserveCollateral<'info> {
pub liquidator: Signer<'info>,
#[account(mut, has_one = lending_market)]
pub obligation: AccountLoader<'info, Obligation>,
pub lending_market: AccountLoader<'info, LendingMarket>,
#[account(
seeds = [seeds::LENDING_MARKET_AUTH, lending_market.key().as_ref()],
bump = lending_market.load()?.bump_seed as u8,
)]
pub lending_market_authority: AccountInfo<'info>,
// Repay reserve (debt being paid off)
#[account(mut, has_one = lending_market)]
pub repay_reserve: AccountLoader<'info, Reserve>,
#[account(address = repay_reserve.load()?.liquidity.mint_pubkey)]
pub repay_reserve_liquidity_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut, address = repay_reserve.load()?.liquidity.supply_vault)]
pub repay_reserve_liquidity_supply: Box<InterfaceAccount<'info, TokenAccount>>,
// Withdraw reserve (collateral being seized)
#[account(mut, has_one = lending_market)]
pub withdraw_reserve: AccountLoader<'info, Reserve>,
#[account(address = withdraw_reserve.load()?.liquidity.mint_pubkey)]
pub withdraw_reserve_liquidity_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut, address = withdraw_reserve.load()?.collateral.mint_pubkey)]
pub withdraw_reserve_collateral_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut, address = withdraw_reserve.load()?.collateral.supply_vault)]
pub withdraw_reserve_collateral_supply: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut, address = withdraw_reserve.load()?.liquidity.supply_vault)]
pub withdraw_reserve_liquidity_supply: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut, address = withdraw_reserve.load()?.liquidity.fee_vault)]
pub withdraw_reserve_liquidity_fee_receiver: Box<InterfaceAccount<'info, TokenAccount>>,
// Liquidator accounts
#[account(mut)]
pub user_source_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut)]
pub user_destination_collateral: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut)]
pub user_destination_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
pub collateral_token_program: Program<'info, Token>,
pub repay_liquidity_token_program: Interface<'info, TokenInterface>,
pub withdraw_liquidity_token_program: Interface<'info, TokenInterface>,
#[account(address = SysInstructions::id())]
pub instruction_sysvar_account: AccountInfo<'info>,
}
Calculate Liquidation Bonus
function calculateLiquidationBonus(
repayAmount: number,
repayReserve: Reserve,
withdrawReserve: Reserve
): { collateralAmount: number; bonus: number } {
// Get prices
const repayPrice = repayReserve.liquidity.marketPrice.toNumber();
const withdrawPrice = withdrawReserve.collateral.marketPrice.toNumber();
// Liquidation bonus (e.g., 5% = 1.05)
const bonusRate = 1 + (withdrawReserve.config.liquidationBonus / 100);
// Value being repaid
const repayValue = repayAmount * repayPrice;
// Collateral value with bonus
const collateralValueWithBonus = repayValue * bonusRate;
// Collateral amount to receive
const collateralAmount = collateralValueWithBonus / withdrawPrice;
// Actual bonus in collateral tokens
const bonus = collateralAmount - (repayValue / withdrawPrice);
return { collateralAmount, bonus };
}
Partial vs Full Liquidations
Partial Liquidation
Liquidate a percentage of the debt (respecting close factor):async function partialLiquidation(
program: Program,
obligation: PublicKey,
closeFactorPct: number = 20 // 20% of debt
) {
const obligationData = await program.account.obligation.fetch(obligation);
// Find largest borrow
let largestBorrow = null;
let maxBorrowValue = 0;
for (const borrow of obligationData.borrows) {
if (borrow.borrowedAmountSf.gt(new BN(0))) {
const borrowValue = borrow.borrowedAmountSf.toNumber();
if (borrowValue > maxBorrowValue) {
maxBorrowValue = borrowValue;
largestBorrow = borrow;
}
}
}
// Calculate liquidation amount (closeFactorPct of largest borrow)
const liquidateAmount = Math.floor(
largestBorrow.borrowedAmountSf.toNumber() * (closeFactorPct / 100)
);
// Execute liquidation
// ...
}
Full Liquidation
Liquidate entire position if severely underwater:async function fullLiquidation(
program: Program,
obligation: PublicKey
) {
const obligationData = await program.account.obligation.fetch(obligation);
// Liquidate all borrows
for (const borrow of obligationData.borrows) {
if (borrow.borrowedAmountSf.gt(new BN(0))) {
// Use u64::MAX to repay entire borrow
await liquidateObligation(
program,
liquidator,
obligation,
borrow.borrowReserve,
// Choose best collateral to seize
selectBestCollateral(obligationData),
lendingMarket,
u64Max,
0
);
}
}
}
Liquidation Bot Example
Monitor Obligations
async function monitorObligations(
program: Program,
lendingMarket: PublicKey
): Promise<PublicKey[]> {
// Get all obligations for the lending market
const obligations = await program.account.obligation.all([
{
memcmp: {
offset: 8 + 32, // Skip discriminator + last_update
bytes: lendingMarket.toBase58(),
},
},
]);
const unhealthy: PublicKey[] = [];
for (const { publicKey, account } of obligations) {
// Fetch all deposit and borrow reserves
const reserves = await fetchReserves(program, account);
if (isLiquidatable(account, reserves)) {
unhealthy.push(publicKey);
console.log(`Found liquidatable obligation: ${publicKey.toString()}`);
}
}
return unhealthy;
}
Calculate Optimal Liquidation
function calculateOptimalLiquidation(
obligation: Obligation,
reserves: Map<string, Reserve>
): { repayReserve: PublicKey; withdrawReserve: PublicKey; amount: number } {
// Find largest borrow
let largestBorrow = null;
let maxValue = 0;
for (const borrow of obligation.borrows) {
if (borrow.borrowedAmountSf.gt(new BN(0))) {
const reserve = reserves.get(borrow.borrowReserve.toString());
const value = borrow.borrowedAmountSf.toNumber() * reserve.liquidity.marketPrice.toNumber();
if (value > maxValue) {
maxValue = value;
largestBorrow = borrow;
}
}
}
// Find best collateral to seize (highest bonus)
let bestCollateral = null;
let maxBonus = 0;
for (const deposit of obligation.deposits) {
if (deposit.depositedAmount.gt(new BN(0))) {
const reserve = reserves.get(deposit.depositReserve.toString());
const bonus = reserve.config.liquidationBonus;
if (bonus > maxBonus) {
maxBonus = bonus;
bestCollateral = deposit;
}
}
}
// Calculate liquidation amount (respect close factor)
const closeFactor = 0.5; // 50%
const amount = Math.floor(
largestBorrow.borrowedAmountSf.toNumber() * closeFactor
);
return {
repayReserve: largestBorrow.borrowReserve,
withdrawReserve: bestCollateral.depositReserve,
amount,
};
}
Execute Liquidation
async function executeLiquidation(
program: Program,
liquidator: Keypair,
obligation: PublicKey,
lendingMarket: PublicKey
) {
try {
const obligationData = await program.account.obligation.fetch(obligation);
const reserves = await fetchReserves(program, obligationData);
if (!isLiquidatable(obligationData, reserves)) {
console.log('Obligation is healthy, skipping');
return;
}
const { repayReserve, withdrawReserve, amount } = calculateOptimalLiquidation(
obligationData,
reserves
);
console.log(`Liquidating ${amount} from ${obligation.toString()}`);
await liquidateObligation(
program,
liquidator,
obligation,
repayReserve,
withdrawReserve,
lendingMarket,
amount,
0 // min acceptable
);
console.log('Liquidation successful!');
} catch (error) {
console.error('Liquidation failed:', error);
}
}
Run Bot Loop
async function runLiquidationBot(
program: Program,
liquidator: Keypair,
lendingMarket: PublicKey
) {
console.log('Starting liquidation bot...');
while (true) {
try {
// Find unhealthy obligations
const unhealthy = await monitorObligations(program, lendingMarket);
// Execute liquidations
for (const obligation of unhealthy) {
await executeLiquidation(
program,
liquidator,
obligation,
lendingMarket
);
}
// Wait before next iteration
await new Promise(resolve => setTimeout(resolve, 10000)); // 10s
} catch (error) {
console.error('Bot error:', error);
}
}
}
Error Handling
try {
await liquidateObligation(/* ... */);
} catch (error) {
if (error.message.includes('ObligationHealthy')) {
console.error('Cannot liquidate healthy obligation');
} else if (error.message.includes('LiquidationTooSmall')) {
console.error('Liquidation amount below minimum');
} else if (error.message.includes('InvalidAmount')) {
console.error('Invalid liquidation amount');
}
}
Best Practices
Monitor Multiple Markets
Monitor Multiple Markets
Run bots across different lending markets to maximize opportunities.
Optimize Gas Costs
Optimize Gas Costs
Batch liquidations when possible and use priority fees strategically.
Handle MEV
Handle MEV
Be aware of MEV (Maximal Extractable Value) and use private RPCs if needed.
Risk Management
Risk Management
Set slippage limits with
min_acceptable_received_liquidity_amount to protect against price manipulation.Reserve Selection
Reserve Selection
Choose collateral with highest liquidation bonus and sufficient liquidity for quick exit.
Next Steps
Setup Guide
Review integration setup
Borrow & Repay
Understand borrowing mechanics