Skip to main content

A multiply deposit creates a leveraged collateral position in a single, atomic transaction by combining an initial deposit with temporary liquidity.

When a multiply deposit is initiated, the protocol first temporarily borrows the debt token (for example, USDC) through a flash loan. The borrowed amount is swapped into the collateral token (for example, SOL) using KSwap (Kamino’s token swap SDK). The resulting collateral is then deposited into Kamino Lend. With the collateral in place, the position borrows the debt token against it, and the borrowed amount is used to repay the flash loan before the transaction completes. The result is a leveraged position with increased exposure to the collateral token and an outstanding debt balance.

Multiply Deposit with KSwap

Create a leveraged position in a single atomic transaction using Klend’s multiply feature with KSwap multi-route swap optimization.
1

Import Dependencies

Import the required packages for Solana RPC communication, Kamino SDK operations, KSwap routing, and transaction building.
import {
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  address,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
  none,
  compressTransactionMessageUsingAddressLookupTables,
} from '@solana/kit';
import type { Address, Instruction } from '@solana/kit';
import {
  KaminoMarket,
  MultiplyObligation,
  PROGRAM_ID,
  parseKeypairFile,
  getDepositWithLeverageIxs,
  getUserLutAddressAndSetupIxs,
  getScopeRefreshIxForObligationAndReserves,
  getComputeBudgetAndPriorityFeeIxs,
  ObligationTypeTag,
  DEFAULT_RECENT_SLOT_DURATION_MS,
} from '@kamino-finance/klend-sdk';
import { KswapSdk } from '@kamino-finance/kswap-sdk';
import { Scope } from '@kamino-finance/scope-sdk';
import Decimal from 'decimal.js';
import { getKswapQuoter, getKswapSwapper, getTokenPriceWithFallback } from './kswap_utils.js';
import { fetchAllAddressLookupTable } from '@solana-program/address-lookup-table';
2

Load Configuration and Initialize SDKs

Load the keypair and initialize RPC connections, the market, Scope for oracle prices, and KSwap for routing.
const KEYPAIR_FILE = '/path/to/your/keypair.json';
const CDN_ENDPOINT = 'https://cdn.kamino.finance';

const signer = await parseKeypairFile(KEYPAIR_FILE);

const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');

const marketPubkey = address('7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF');
const collTokenMint = address('So11111111111111111111111111111111111111112');
const debtTokenMint = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const MAIN_MARKET_LUT = address('GprZNyWk67655JhX6Rq9KoebQ6WkQYRhATWzkx2P2LNc');

const market = await KaminoMarket.load(rpc, marketPubkey, DEFAULT_RECENT_SLOT_DURATION_MS);
const scope = new Scope('mainnet-beta', rpc);
const kswapSdk = new KswapSdk('https://api.kamino.finance/kswap', rpc, rpcSubscriptions);
Replace /path/to/your/keypair.json with the desired keypair path. The Scope SDK provides oracle price data, and the KSwap SDK enables multi-DEX routing for optimal swap execution.
3

Fetch Multiply Lookup Tables

Fetch multiply-specific Address Lookup Tables (LUTs) from Kamino’s CDN to compress transaction size.
const kaminoResourcesResponse = await fetch(`${CDN_ENDPOINT}/resources.json`);
const kaminoResourcesData = await kaminoResourcesResponse.json();
const kaminoResources = kaminoResourcesData['mainnet-beta'];
const multiplyColPairs = kaminoResources.multiplyLUTsPairs[collTokenMint] || {};
const multiplyLut: string[] = multiplyColPairs[debtTokenMint] || [];
const multiplyLutKeys = multiplyLut.map((lut) => address(lut));
Lookup Tables store frequently-used addresses, allowing transactions to reference 1-byte indexes instead of 32-byte addresses. This reduces transaction size from ~3096 bytes to under 1232 bytes, which is critical for complex multiply operations.
4

Configure Multiply Parameters

Set the deposit amount, leverage multiplier, and slippage tolerance.
const depositAmount = new Decimal(1);
const leverage = 3;
const slippageBps = 30;
5

Setup User Lookup Table

Generate setup instructions for creating or extending a user-specific lookup table with addresses needed for multiply operations.
const multiplyMints: { coll: Address; debt: Address }[] = [{ coll: collTokenMint, debt: debtTokenMint }];
const leverageMints: { coll: Address; debt: Address }[] = [];

const [userLookupTable, setupTxsIxs] = await getUserLutAddressAndSetupIxs(
  market!,
  signer,
  none(),
  true,
  multiplyMints,
  leverageMints
);
Execute the setup transactions if any are needed. The loop builds, signs, and sends each setup transaction, waiting 2 seconds between each to ensure proper ordering.
for (const setupIxs of setupTxsIxs) {
  const { value: setupBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();

  const setupTx = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(signer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(setupBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(setupIxs, tx)
  );

  const signedSetupTx = await signTransactionMessageWithSigners(setupTx);

  await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedSetupTx, {
    commitment: 'confirmed',
    skipPreflight: true,
  });

  await new Promise((resolve) => setTimeout(resolve, 2000));
}
Setup transactions extend the lookup table with addresses specific to the multiply position. This is a one-time operation per collateral/debt pair - if the LUT already exists, setupTxsIxs will be empty and the loop won’t execute.
6

Check for Existing Obligation

Check if a multiply obligation already exists for this collateral/debt pair.
const currentSlot = await rpc.getSlot().send();
const collTokenReserve = market!.getReserveByMint(collTokenMint)!;
const debtTokenReserve = market!.getReserveByMint(debtTokenMint)!;

const obligationAddress = await new MultiplyObligation(collTokenMint, debtTokenMint, PROGRAM_ID).toPda(
  market!.getAddress(),
  signer.address
);
const obligation = await market!.getObligationByAddress(obligationAddress);
If an obligation exists, the transaction will add to the existing position. If null, a new obligation will be created using a flash loan to bootstrap the position.
7

Get Scope Price Refresh Instructions

Fetch latest oracle prices for accurate collateral and debt valuation.
const scopeConfiguration = { scope, scopeConfigurations: await scope.getAllConfigurations() };

const scopeRefreshIx = await getScopeRefreshIxForObligationAndReserves(
  market!,
  collTokenReserve,
  debtTokenReserve,
  obligation!,
  scopeConfiguration
);
8

Build Multi-Route Swap Instructions

Fetch the latest USDC/SOL price ratio to keep swap calculations accurate. Set the transaction compute budget and priority fee for reliable execution. Then use KSwap to generate multiple swap route options, with the quoter and swapper helper functions finding optimal paths. See the Helper Functions section below for the implementation of these utilities.
const priceDebtToColl = new Decimal(await getTokenPriceWithFallback(kswapSdk, debtTokenMint, collTokenMint));

const computeIxs = getComputeBudgetAndPriorityFeeIxs(1_400_000, new Decimal(500000));

const routes = await getDepositWithLeverageIxs({
  owner: signer,
  kaminoMarket: market!,
  debtTokenMint: debtTokenMint,
  collTokenMint: collTokenMint,
  depositAmount: depositAmount,
  priceDebtToColl: priceDebtToColl,
  slippagePct: new Decimal(slippageBps / 100),
  obligation: obligation || null,
  referrer: none(),
  currentSlot,
  targetLeverage: new Decimal(leverage),
  selectedTokenMint: debtTokenMint,
  obligationTypeTagOverride: ObligationTypeTag.Multiply,
  scopeRefreshIx,
  budgetAndPriorityFeeIxs: computeIxs,
  quoteBufferBps: new Decimal(1000),
  quoter: getKswapQuoter(kswapSdk, signer.address, slippageBps, debtTokenReserve, collTokenReserve),
  swapper: getKswapSwapper(kswapSdk, signer.address, slippageBps, debtTokenReserve, collTokenReserve),
  useV2Ixs: true,
});
KSwap SDK v2 simulates routes server-side, providing simulation data that can be used for manual route filtering.
9

Filter and Select Best Route

Prepare the Klend lookup tables for transaction compression.
const klendLookupTableKeys: Address[] = [];
klendLookupTableKeys.push(userLookupTable);
klendLookupTableKeys.push(...multiplyLutKeys);
klendLookupTableKeys.push(MAIN_MARKET_LUT);

const klendLutAccounts = await fetchAllAddressLookupTable(rpc, klendLookupTableKeys);
Check that KSwap returned routes.
if (routes.length === 0) {
  throw new Error('No routes found from KSwap');
}
Select the route with the smallest transaction size to maximize the chance of fitting within Solana’s transaction size limit.
const bestRouteData = routes.reduce((best, current) => {
  const bestTotalIxsSize = best.ixs.reduce((total: number, ix: Instruction) => {
    if (!ix?.accounts || !ix?.data) {
      console.warn(`Skipping invalid instruction in best route`);
      return total;
    }
    return total + ix.accounts.length * 32 + ix.data.byteLength + 1;
  }, 0);

  const currentTotalIxsSize = current.ixs.reduce((total: number, ix: Instruction) => {
    if (!ix?.accounts || !ix?.data) {
      return total;
    }
    return total + ix.accounts.length * 32 + ix.data.byteLength + 1;
  }, 0);

  return bestTotalIxsSize <= currentTotalIxsSize ? best : current;
});
Combine the route’s lookup tables with Klend’s lookup tables.
const lookupTables = bestRouteData.lookupTables;
lookupTables.push(...klendLutAccounts);

const bestRoute = {
  ixs: bestRouteData.ixs,
  luts: lookupTables.map((l) => l.address),
};

console.log(`Selected route: ${bestRouteData.quote!.routerType}`);
The best route is selected based on transaction size rather than price, as fitting within the 1232-byte limit is critical for transaction success.
10

Fetch Blockhash and Load Lookup Tables

Fetch a fresh blockhash for the transaction.
await new Promise((resolve) => setTimeout(resolve, 2000));

const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
Prepare lookup table addresses and build a mapping of LUT address to account addresses.
const lutsByAddress: Record<Address, Address[]> = {};
const bestRouteLutAccounts = await fetchAllAddressLookupTable(rpc, bestRoute.luts || []);

const allLutAddresses = new Set<Address>();
for (const acc of bestRouteLutAccounts) {
  lutsByAddress[acc.address] = acc.data.addresses;
  acc.data.addresses.forEach((addr) => allLutAddresses.add(addr));
}
Extract all account addresses used in the transaction instructions.
const instructionAccounts = new Set<Address>();
bestRoute.ixs.forEach((ix: Instruction) => {
  if (ix?.accounts) {
    ix.accounts.forEach((acc) => {
      if (acc?.address) {
        instructionAccounts.add(acc.address);
      }
    });
  }
});
Compare instruction accounts against existing LUT addresses to find any missing accounts that need additional LUTs.
const accountsNotInLuts = Array.from(instructionAccounts).filter((addr) => !allLutAddresses.has(addr));
If missing accounts are found, call the Kamino API to find the minimal set of additional LUTs that contain those addresses.
if (accountsNotInLuts.length > 0) {
  console.log(`Finding minimal LUTs for ${accountsNotInLuts.length} missing accounts...`);

  const lutFinderResponse = await fetch('https://api.kamino.finance/luts/find-minimal', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      addresses: accountsNotInLuts,
      verify: false,
    }),
  });

  const lutFinderResult = await lutFinderResponse.json();
  const additionalLuts: string[] = lutFinderResult.lutAddresses || [];

  console.log(`Found ${additionalLuts.length} additional LUT(s)`);
Fetch the additional LUTs and add them to the transaction’s lookup table collection.
  if (additionalLuts.length > 0) {
    const additionalLutAddresses = additionalLuts.map((lut) => address(lut));
    const additionalLutAccounts = await fetchAllAddressLookupTable(rpc, additionalLutAddresses);

    for (const acc of additionalLutAccounts) {
      lutsByAddress[acc.address] = acc.data.addresses;
      acc.data.addresses.forEach((addr) => allLutAddresses.add(addr));
    }

    lookupTables.push(...additionalLutAccounts);
  }
}
Build, sign, and send the transaction.
const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  (tx) => appendTransactionMessageInstructions(bestRoute.ixs, tx),
  (tx) => setTransactionMessageFeePayerSigner(signer, tx),
  (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
  (tx) => compressTransactionMessageUsingAddressLookupTables(tx, lutsByAddress)
);

const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);

await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
  commitment: 'processed',
  preflightCommitment: 'processed',
  skipPreflight: true,
});

console.log(`Multiply deposit successful! Signature: ${signature}`);
The multiply deposit transaction is complete, resulting in a leveraged position with 3x exposure, all executed atomically in a single transaction.

Helper Functions

The tutorial requires KSwap helper functions for price fetching and route building.
import BN from 'bn.js';
import { KaminoReserve } from '@kamino-finance/klend-sdk';
import type { SwapInputs, SwapQuote, SwapIxs, SwapIxsProvider, SwapQuoteProvider } from '@kamino-finance/klend-sdk';
import { KswapSdk, RouterContext } from '@kamino-finance/kswap-sdk';
import type { RouteOutput, RouteParams, RouterType, MintInfo } from '@kamino-finance/kswap-sdk';
import Decimal from 'decimal.js';
import type { Address } from '@solana/kit';

const ALLOWED_ROUTERS: RouterType[] = ['metis', 'titan', 'dflow', 'openOcean', 'jupiterLite'];

export async function getTokenPriceWithFallback(
  kswapSdk: KswapSdk,
  inputMint: Address,
  outputMint: Address
): Promise<number> {
  const params = {
    ids: inputMint.toString(),
    vsToken: outputMint.toString(),
  };
  const res = await kswapSdk.getPriceWithFallback(params);

  return Number(res[inputMint.toString()]?.usdPrice || 0);
}

export function getKswapQuoter(
  kswapSdk: KswapSdk,
  executor: Address,
  slippageBps: number,
  inputMintReserve: KaminoReserve,
  outputMintReserve: KaminoReserve
): SwapQuoteProvider<RouteOutput> {
  const quoter: SwapQuoteProvider<RouteOutput> = async (inputs: SwapInputs): Promise<SwapQuote<RouteOutput>> => {
    const inMintInfo: MintInfo = {
      tokenProgramId: inputMintReserve.getLiquidityTokenProgram(),
      decimals: inputMintReserve.stats.decimals,
    };
    const outMintInfo: MintInfo = {
      tokenProgramId: outputMintReserve.getLiquidityTokenProgram(),
      decimals: outputMintReserve.stats.decimals,
    };
    const routerContext = new RouterContext(inMintInfo, outMintInfo);

    const routeParams: RouteParams = {
      executor: executor,
      tokenIn: inputs.inputMint,
      tokenOut: inputs.outputMint,
      amount: new BN(inputs.inputAmountLamports.toDP(0).toString()),
      maxSlippageBps: slippageBps,
      wrapAndUnwrapSol: false,
      swapType: 'exactIn',
      routerTypes: ALLOWED_ROUTERS,
      includeRfq: false,
      includeLimoLogs: false,
      withSimulation: true,
      filterFailedSimulations: false,
      timeoutMs: 30000,
      atLeastOneNoMoreThanTimeoutMS: 10000,
      preferredMaxAccounts: 10,
    };

    const routeOutputs = await kswapSdk.getAllRoutes(routeParams, routerContext);

    if (routeOutputs.routes.length === 0) {
      throw new Error('No routes found from KSwap. Try increasing preferredMaxAccount.');
    }

    const bestRoute = routeOutputs.routes.reduce((best, current) => {
      const inAmountBest = new Decimal(best.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor());
      const minAmountOutBest = new Decimal(best.amountsExactIn.amountOutGuaranteed.toString()).div(
        outputMintReserve.getMintFactor()
      );
      const priceAInBBest = minAmountOutBest.div(inAmountBest);
      const inAmountCurrent = new Decimal(current.amountsExactIn.amountIn.toString()).div(
        inputMintReserve.getMintFactor()
      );
      const minAmountOutCurrent = new Decimal(current.amountsExactIn.amountOutGuaranteed.toString()).div(
        outputMintReserve.getMintFactor()
      );
      const priceAInBCurrent = minAmountOutCurrent.div(inAmountCurrent);
      return priceAInBBest.greaterThan(priceAInBCurrent) ? best : current;
    });

    const inAmountBest = new Decimal(bestRoute.amountsExactIn.amountIn.toString()).div(
      inputMintReserve.getMintFactor()
    );
    const minAmountOutBest = new Decimal(bestRoute.amountsExactIn.amountOutGuaranteed.toString()).div(
      outputMintReserve.getMintFactor()
    );
    const priceAInBBest = minAmountOutBest.div(inAmountBest);

    return {
      priceAInB: priceAInBBest,
      quoteResponse: bestRoute,
    };
  };

  return quoter;
}

export function getKswapSwapper(
  kswapSdk: KswapSdk,
  executor: Address,
  slippageBps: number,
  inputMintReserve: KaminoReserve,
  outputMintReserve: KaminoReserve
): SwapIxsProvider<RouteOutput> {
  const swapper: SwapIxsProvider<RouteOutput> = async (inputs: SwapInputs): Promise<Array<SwapIxs<RouteOutput>>> => {
    const inMintInfo: MintInfo = {
      tokenProgramId: inputMintReserve.getLiquidityTokenProgram(),
      decimals: inputMintReserve.stats.decimals,
    };
    const outMintInfo: MintInfo = {
      tokenProgramId: outputMintReserve.getLiquidityTokenProgram(),
      decimals: outputMintReserve.stats.decimals,
    };
    const routerContext = new RouterContext(inMintInfo, outMintInfo);

    const routeParams: RouteParams = {
      executor: executor,
      tokenIn: inputs.inputMint,
      tokenOut: inputs.outputMint,
      amount: new BN(inputs.inputAmountLamports.toString()),
      maxSlippageBps: slippageBps,
      wrapAndUnwrapSol: false,
      swapType: 'exactIn',
      routerTypes: ALLOWED_ROUTERS,
      includeRfq: false,
      includeLimoLogs: false,
      withSimulation: true,
      filterFailedSimulations: false,
      timeoutMs: 30000,
      atLeastOneNoMoreThanTimeoutMS: 10000,
      preferredMaxAccounts: 10,
    };

    const routeOutputs = await kswapSdk.getAllRoutes(routeParams, routerContext);

    if (routeOutputs.routes.length === 0) {
      throw new Error('No routes found from KSwap in swapper.');
    }

    return routeOutputs.routes.map((routeOutput) => {
      const inAmount = new Decimal(routeOutput.amountsExactIn.amountIn.toString()).div(
        routeOutput.inputTokenDecimals! || inputMintReserve.getMintFactor()
      );
      const minAmountOut = new Decimal(routeOutput.amountsExactIn.amountOutGuaranteed.toString()).div(
        routeOutput.outputTokenDecimals! || outputMintReserve.getMintFactor()
      );
      const priceAInB = minAmountOut.div(inAmount);

      return {
        preActionIxs: [],
        swapIxs: routeOutput.instructions?.swapIxs || [],
        lookupTables: routeOutput.lookupTableAccounts || [],
        quote: {
          priceAInB: priceAInB,
          quoteResponse: routeOutput,
          simulationResult: routeOutput.simulationResult,
          routerType: routeOutput.routerType,
        },
      };
    });
  };

  return swapper;
}

Transaction Flow

Asset Flow

1

Initial State

  • Wallet: 1 USDC
  • Position: None
2

Flash Borrow

  • Flash loan: 2 USDC (temporary)
  • Available: 3 USDC total
3

Swap & Deposit

  • Swap 3 USDC to ~0.032 SOL via KSwap
  • Deposit 0.032 SOL as collateral
4

Borrow & Repay

  • Borrow 2 USDC against collateral
  • Repay 2 USDC flash loan
5

Final Position

  • Collateral: 0.032 SOL ($3 value)
  • Debt: 2 USDC
  • Net Equity: 1 USDC
  • Leverage: 3x

Key Concepts

Flash loans are uncollateralized loans that must be borrowed and repaid within the same transaction. They enable:
  • Zero upfront capital: Borrow large amounts without collateral
  • Atomic execution: All operations succeed or fail together
  • Leverage creation: Bootstrap leveraged positions in one transaction
  • Risk-free: Transaction reverts if flash loan cannot be repaid
The MultiplyObligation is a specialized obligation type that:
  • Defines position type: Multiply vs Leverage vs Vanilla
  • Derives unique address: Via PDA using collateral/debt mints
  • Supports multiple positions: Create different obligations with same tokens using ID parameter
  • Creates new obligation: When none exists (indicated by obligation: null)
Address Lookup Tables reduce transaction size by:
  • Storing common addresses: Frequently-used program accounts, reserves, oracles
  • Index-based references: 1-byte index instead of 32-byte address
  • Transaction compression: Enables complex transactions to fit within the 1232-byte limit
  • Multiply-specific LUTs: Fetched from Kamino CDN per collateral/debt pair
  • Critical for complex txns: Multiply operations wouldn’t fit without LUTs
KSwap provides optimal swap execution through:
  • Route simulation: Tests each route for success server-side
  • Transaction size selection: Prioritizes routes that fit within transaction limits
  • Manual filtering: Provides simulation data for custom route selection logic
  • Slippage protection: Guarantees minimum output amount
Solana commitment levels affect confirmation speed:
  • processed: Fastest, used for sending (no false timeout errors)
  • confirmed: Medium speed, confirmed by majority of cluster stake
  • finalized: Slowest, used for blockhash (maximum security, ~32 blocks)
  • Recommended: Use processed for multiply transactions to avoid false errors