Skip to main content

Overview

To borrow from Kamino Lending, you must:
  1. Initialize an obligation account
  2. Deposit collateral into the obligation
  3. Ensure your health factor remains above liquidation threshold
  4. Refresh your obligation before borrowing or repaying

Initialize Obligation

Create an obligation account to track your borrows and collateral.

Instruction: init_obligation

pub fn process(ctx: Context<InitObligation>, args: InitObligationArgs) -> Result<()> {
    let clock = &Clock::get()?;
    
    // Verify obligation seeds (different tag types for different use cases)
    check_obligation_seeds(
        args.tag,
        &ctx.accounts.seed1_account,
        &ctx.accounts.seed2_account,
    ).unwrap();
    
    let obligation = &mut ctx.accounts.obligation.load_init()?;
    let owner_user_metadata = &ctx.accounts.owner_user_metadata.load()?;
    
    // Initialize obligation with default state
    obligation.init(crate::state::obligation::InitObligationParams {
        current_slot: clock.slot,
        lending_market: ctx.accounts.lending_market.key(),
        owner: ctx.accounts.obligation_owner.key(),
        deposits: [ObligationCollateral::default(); 8], // Max 8 collateral types
        borrows: [ObligationLiquidity::default(); 5],   // Max 5 borrow types
        tag: args.tag as u64,
        referrer: owner_user_metadata.referrer,
    });
    
    Ok(())
}

Account Context

#[derive(Accounts)]
#[instruction(args: InitObligationArgs)]
pub struct InitObligation<'info> {
    pub obligation_owner: Signer<'info>,
    
    #[account(mut)]
    pub fee_payer: Signer<'info>,
    
    #[account(
        init,
        seeds = [
            &[args.tag],
            &[args.id],
            obligation_owner.key().as_ref(),
            lending_market.key().as_ref(),
            seed1_account.key().as_ref(),
            seed2_account.key().as_ref()
        ],
        bump,
        payer = fee_payer,
        space = OBLIGATION_SIZE + 8,
    )]
    pub obligation: AccountLoader<'info, Obligation>,
    
    pub lending_market: AccountLoader<'info, LendingMarket>,
    
    pub seed1_account: AccountInfo<'info>,
    pub seed2_account: AccountInfo<'info>,
    
    #[account(
        seeds = [BASE_SEED_USER_METADATA, obligation_owner.key().as_ref()],
        bump = owner_user_metadata.load()?.bump.try_into().unwrap(),
    )]
    pub owner_user_metadata: AccountLoader<'info, UserMetadata>,
    
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
}

Refresh Obligation

CRITICAL: Always refresh the obligation before borrowing or repaying to ensure accurate health calculations.

Instruction: refresh_obligation

pub fn process(ctx: Context<RefreshObligation>) -> Result<()> {
    let obligation = &mut ctx.accounts.obligation.load_mut()?;
    let clock = &Clock::get()?;
    let lending_market = &ctx.accounts.lending_market.load()?;
    
    let borrow_count = obligation.active_borrows_count();
    let deposit_count = obligation.active_deposits_count();
    let reserves_count = borrow_count + deposit_count;
    
    // Remaining accounts must include:
    // - All deposit reserves (first)
    // - All borrow reserves (second)
    // - Referrer token states if referrer exists (last)
    let expected_remaining_accounts = if obligation.has_referrer() {
        reserves_count + borrow_count
    } else {
        reserves_count
    };
    
    require!(
        ctx.remaining_accounts.len() == expected_remaining_accounts,
        LendingError::InvalidAccountInput
    );
    
    let deposit_reserves_iter = ctx.remaining_accounts
        .iter()
        .take(deposit_count)
        .map(|ai| FatAccountLoader::<Reserve>::try_from(ai).unwrap());
    
    let borrow_reserves_iter = ctx.remaining_accounts
        .iter()
        .skip(deposit_count)
        .take(borrow_count)
        .map(|ai| FatAccountLoader::<Reserve>::try_from(ai).unwrap());
    
    let referrer_token_states_iter = ctx.remaining_accounts
        .iter()
        .skip(reserves_count)
        .map(|ai| FatAccountLoader::<ReferrerTokenState>::try_from(ai).unwrap());
    
    lending_operations::refresh_obligation(
        &crate::ID,
        obligation,
        lending_market,
        clock,
        deposit_reserves_iter,
        borrow_reserves_iter,
        referrer_token_states_iter,
    )?;
    
    Ok(())
}

Borrow Obligation Liquidity

Borrow tokens against your deposited collateral.

Instruction: borrow_obligation_liquidity_v2

pub fn borrow_obligation_liquidity_process_impl<'info>(
    accounts: &BorrowObligationLiquidity<'info>,
    remaining_accounts: &[AccountInfo<'info>],
    borrow_size: BorrowSize,
) -> Result<u64> {
    lending_checks::borrow_obligation_liquidity_checks(accounts)?;
    
    let borrow_reserve = &mut accounts.borrow_reserve.load_mut()?;
    let lending_market = &accounts.lending_market.load()?;
    let obligation = &mut accounts.obligation.load_mut()?;
    let clock = &Clock::get()?;
    
    // Get all deposit reserves for health check
    let deposit_reserves_iter = remaining_accounts
        .iter()
        .map(|ai| FatAccountLoader::<Reserve>::try_from(ai).unwrap());
    
    // Calculate borrow with origination fees
    let CalculateBorrowResult {
        receive_amount,
        origination_fee,
        ..
    } = lending_operations::borrow_obligation_liquidity(
        lending_market,
        borrow_reserve,
        obligation,
        borrow_size,
        clock,
        accounts.borrow_reserve.key(),
        referrer_token_state_option,
        deposit_reserves_iter,
    )?;
    
    // Transfer origination fee to fee receiver
    if origination_fee > 0 {
        token_transfer::send_origination_fees_transfer(
            // Transfers origination_fee from reserve to fee_receiver
        )?;
    }
    
    // Transfer borrowed amount to user
    let lending_market_key = accounts.lending_market.key();
    let authority_signer_seeds = gen_signer_seeds!(
        lending_market_key.as_ref(),
        lending_market.bump_seed as u8
    );
    
    token_transfer::borrow_obligation_liquidity_transfer(
        accounts.token_program.to_account_info(),
        accounts.borrow_reserve_liquidity_mint.to_account_info(),
        accounts.reserve_source_liquidity.to_account_info(),
        accounts.user_destination_liquidity.to_account_info(),
        accounts.lending_market_authority.to_account_info(),
        authority_signer_seeds,
        receive_amount,
        accounts.borrow_reserve_liquidity_mint.decimals,
    )?;
    
    Ok(receive_amount)
}

Account Context

#[derive(Accounts)]
pub struct BorrowObligationLiquidity<'info> {
    pub owner: Signer<'info>,
    
    #[account(
        mut,
        has_one = lending_market,
        has_one = owner @ LendingError::InvalidObligationOwner
    )]
    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>,
    
    #[account(mut, has_one = lending_market)]
    pub borrow_reserve: AccountLoader<'info, Reserve>,
    
    #[account(
        address = borrow_reserve.load()?.liquidity.mint_pubkey,
        mint::token_program = token_program,
    )]
    pub borrow_reserve_liquidity_mint: Box<InterfaceAccount<'info, Mint>>,
    
    #[account(mut, address = borrow_reserve.load()?.liquidity.supply_vault)]
    pub reserve_source_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
    
    #[account(mut, address = borrow_reserve.load()?.liquidity.fee_vault)]
    pub borrow_reserve_liquidity_fee_receiver: Box<InterfaceAccount<'info, TokenAccount>>,
    
    #[account(
        mut,
        token::mint = reserve_source_liquidity.mint,
        token::authority = owner,
    )]
    pub user_destination_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
    
    #[account(mut)]
    pub referrer_token_state: Option<AccountLoader<'info, ReferrerTokenState>>,
    
    pub token_program: Interface<'info, TokenInterface>,
    
    #[account(address = SysInstructions::id())]
    pub instruction_sysvar_account: AccountInfo<'info>,
}

Repay Obligation Liquidity

Repay borrowed tokens to reduce debt and improve health factor.

Instruction: repay_obligation_liquidity_v2

pub(super) fn process_impl<'a, 'info>(
    accounts: &RepayObligationLiquidity,
    remaining_accounts: impl Iterator<Item = &'a AccountInfo<'info>>,
    liquidity_amount: u64,
) -> Result<()>
where
    'info: 'a,
{
    lending_checks::repay_obligation_liquidity_checks(accounts)?;
    let clock = Clock::get()?;
    
    let repay_reserve = &mut accounts.repay_reserve.load_mut()?;
    let obligation = &mut accounts.obligation.load_mut()?;
    let lending_market = &accounts.lending_market.load()?;
    
    // Calculate actual repay amount (may be less if paying off full debt)
    let repay_amount = lending_operations::repay_obligation_liquidity(
        repay_reserve,
        obligation,
        &clock,
        liquidity_amount,
        accounts.repay_reserve.key(),
        lending_market,
        remaining_accounts.map(|a| {
            FatAccountLoader::try_from(a).expect("Invalid deposit reserve")
        }),
    )?;
    
    // Transfer repayment from user to reserve
    token_transfer::repay_obligation_liquidity_transfer(
        accounts.token_program.to_account_info(),
        accounts.reserve_liquidity_mint.to_account_info(),
        accounts.user_source_liquidity.to_account_info(),
        accounts.reserve_destination_liquidity.to_account_info(),
        accounts.owner.to_account_info(),
        repay_amount,
        accounts.reserve_liquidity_mint.decimals,
    )?;
    
    Ok(())
}

Account Context

#[derive(Accounts)]
pub struct RepayObligationLiquidity<'info> {
    pub owner: Signer<'info>,
    
    #[account(
        mut,
        has_one = lending_market,
        constraint = obligation.load()?.lending_market == repay_reserve.load()?.lending_market
    )]
    pub obligation: AccountLoader<'info, Obligation>,
    
    pub lending_market: AccountLoader<'info, LendingMarket>,
    
    #[account(mut, has_one = lending_market)]
    pub repay_reserve: AccountLoader<'info, Reserve>,
    
    #[account(
        address = repay_reserve.load()?.liquidity.mint_pubkey,
        mint::token_program = token_program,
    )]
    pub reserve_liquidity_mint: Box<InterfaceAccount<'info, Mint>>,
    
    #[account(mut, address = repay_reserve.load()?.liquidity.supply_vault)]
    pub reserve_destination_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
    
    #[account(mut, token::mint = repay_reserve.load()?.liquidity.mint_pubkey)]
    pub user_source_liquidity: Box<InterfaceAccount<'info, TokenAccount>>,
    
    pub token_program: Interface<'info, TokenInterface>,
    
    #[account(
        address = SysInstructions::id(),
        constraint = ix_utils::no_restricted_programs_within_tx(&instruction_sysvar_account)?
            @ LendingError::TransactionIncludesRestrictedPrograms
    )]
    pub instruction_sysvar_account: AccountInfo<'info>,
}

Health Factor & Collateral Requirements

Your obligation must maintain a healthy loan-to-value (LTV) ratio:
// Calculate obligation health
function calculateHealth(obligation: Obligation, reserves: Reserve[]): number {
  let totalBorrowValue = 0;
  let totalCollateralValue = 0;
  
  // Sum borrow values
  for (const borrow of obligation.borrows) {
    if (borrow.borrowedAmountSf.gt(new BN(0))) {
      const reserve = reserves.find(r => r.pubkey.equals(borrow.borrowReserve));
      totalBorrowValue += borrow.borrowedAmountSf * reserve.price;
    }
  }
  
  // Sum collateral values (weighted by LTV)
  for (const deposit of obligation.deposits) {
    if (deposit.depositedAmount.gt(new BN(0))) {
      const reserve = reserves.find(r => r.pubkey.equals(deposit.depositReserve));
      totalCollateralValue += deposit.depositedAmount * reserve.price * reserve.config.loanToValuePct;
    }
  }
  
  // Health factor: collateral / borrows
  // < 1.0 = unhealthy (liquidatable)
  // >= 1.0 = healthy
  return totalCollateralValue / totalBorrowValue;
}

Complete Borrow Flow

1

Initialize Obligation

Create obligation account if you don’t have one
2

Deposit Collateral

Deposit cTokens into your obligation using deposit_obligation_collateral_v2
3

Refresh Obligation

Call refresh_obligation to update prices and accrued interest
4

Borrow

Call borrow_obligation_liquidity_v2 with desired amount
5

Monitor Health

Regularly check your health factor to avoid liquidation
6

Repay When Ready

Call repay_obligation_liquidity_v2 to reduce debt

Error Handling

try {
  await borrowObligationLiquidity(/* ... */);
} catch (error) {
  if (error.message.includes('BorrowTooLarge')) {
    console.error('Borrow amount exceeds max LTV');
  } else if (error.message.includes('ObligationCollateralEmpty')) {
    console.error('No collateral deposited');
  } else if (error.message.includes('InsufficientLiquidity')) {
    console.error('Reserve has insufficient liquidity');
  }
}

Next Steps

Liquidations

Learn about liquidation mechanics and building bots

Deposit & Withdraw

Review collateral management

Build docs developers (and LLMs) love