Multiply Deposit
A multiply deposit creates a leveraged position in a single atomic transaction by combining an initial deposit with a flash loan.
When a multiply deposit is initiated, the protocol temporarily borrows additional capital through a flash loan, which is combined with the initial deposit. This aggregated capital is then swapped for the target collateral token via KSwap and deposited into Kamino.
With the collateral securing the position, the protocol borrows the amount needed to repay the flash loan. The flash loan is repaid within the same transaction, leaving a leveraged position backed by more collateral than the initial deposit could have purchased directly.
Multiply Deposit with KSwap
Execute a multiply deposit with multi-route swap optimization.
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 {
KaminoMarket ,
MultiplyObligation ,
PROGRAM_ID ,
parseKeypairFile ,
getDepositWithLeverageIxs ,
getUserLutAddressAndSetupIxs ,
getScopeRefreshIxForObligationAndReserves ,
getComputeBudgetAndPriorityFeeIxs ,
ObligationTypeTag ,
simulateTx ,
} from '@kamino-finance/klend-sdk' ;
import { KswapSdk } from '@kamino-finance/kswap-sdk' ;
import { Scope } from '@kamino-finance/scope-sdk' ;
import { Connection } from '@solana/web3.js' ;
import Decimal from 'decimal.js' ;
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 connection = new Connection ( 'https://api.mainnet-beta.solana.com' );
// JLP Market configuration
const marketPubkey = address ( 'DxXdAyU3kCjnyggvHmY5nAwg5cRbbmdyX3npfDMjjMek' );
const collTokenMint = address ( '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4' ); // JLP
const debtTokenMint = address ( 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' ); // USDC
const market = await KaminoMarket . load ( rpc , marketPubkey , 100 );
const scope = new Scope ( 'mainnet-beta' , rpc );
const kswapSdk = new KswapSdk ( 'https://api.kamino.finance/kswap' , connection );
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.
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 = multiplyColPairs [ debtTokenMint ] || [];
const multiplyLutKeys = multiplyLut . map (( lut : string ) => 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.
Set the deposit amount, leverage multiplier, and slippage tolerance. const depositAmount = new Decimal ( 1 ); // $1 USDC
const leverage = 3 ; // 3x leverage
const slippageBps = 30 ; // 0.3% slippage tolerance
Setup User Lookup Table Create and extend a user-specific lookup table with required addresses 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 setup transactions if needed
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.
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 obligationType = new MultiplyObligation (
collTokenMint ,
debtTokenMint ,
PROGRAM_ID
);
const obligationAddress = await obligationType . 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.
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
);
Build Multi-Route Swap Instructions Generate multiple swap routes and select the best one based on price simulation. import { getKswapQuoter , getKswapSwapper , getTokenPriceFromJupWithFallback } from './kswap_utils' ;
const priceDebtToColl = new Decimal (
await getTokenPriceFromJupWithFallback ( kswapSdk , debtTokenMint , collTokenMint )
);
const computeIxs = getComputeBudgetAndPriorityFeeIxs ( 1_400_000 , new Decimal ( 500000 ));
const depositWithLeverageRoutes = 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 ),
useV2Ixs: true ,
});
KSwap queries multiple DEXs (Jupiter, Dflow, OKX) and generates swap routes. The SDK will simulate each route and select the one with the best effective price.
Simulate Routes and Select Best Simulate all routes and select the one with the best price, or fall back to the first route. import { fetchAllAddressLookupTable } from '@solana-program/address-lookup-table' ;
const JLP_MARKET_LUT = address ( 'GprZNyWk67655JhX6Rq9KoebQ6WkQYRhATWzkx2P2LNc' );
const klendLookupTableKeys = [ userLookupTable , ... multiplyLutKeys , JLP_MARKET_LUT ];
const klendLutAccounts = await fetchAllAddressLookupTable ( rpc , klendLookupTableKeys );
const simulationResults = await Promise . all (
depositWithLeverageRoutes . map ( async ( route ) => {
const lookupTables = [ ... route . lookupTables , ... klendLutAccounts ];
const simulation = await simulateTx (
rpc ,
signer . address ,
route . ixs ,
lookupTables
). catch (() => undefined );
if ( ! simulation || simulation . value . err ) {
return undefined ;
}
return {
ixs: route . ixs ,
luts: lookupTables . map (( l ) => l . address ),
routeOutput: route . quote ! ,
swapInputs: route . swapInputs ,
};
})
);
const passingSimulations = simulationResults . filter (( tx ) => tx !== undefined );
const bestRoute = passingSimulations . length > 0
? passingSimulations . reduce (( best , current ) => {
const inputMintReserve = market . getReserveByMint ( best . swapInputs . inputMint ) ! ;
const outputMintReserve = market . getReserveByMint ( best . swapInputs . outputMint ) ! ;
const bestPrice = new Decimal ( best . routeOutput . amountsExactIn . amountOutGuaranteed . toString ())
. div ( outputMintReserve . getMintFactor ())
. div ( new Decimal ( best . routeOutput . amountsExactIn . amountIn . toString ())
. div ( inputMintReserve . getMintFactor ()));
const currentPrice = new Decimal ( current . routeOutput . amountsExactIn . amountOutGuaranteed . toString ())
. div ( outputMintReserve . getMintFactor ())
. div ( new Decimal ( current . routeOutput . amountsExactIn . amountIn . toString ())
. div ( inputMintReserve . getMintFactor ()));
return bestPrice . greaterThan ( currentPrice ) ? best : current ;
})
: (() => {
const lookupTables = [ ... depositWithLeverageRoutes [ 0 ]. lookupTables , ... klendLutAccounts ];
return {
ixs: depositWithLeverageRoutes [ 0 ]. ixs ,
luts: lookupTables . map (( l ) => l . address )
};
})();
Route simulation tests each swap path to ensure it will succeed on-chain. The best route is selected based on effective price (output amount ÷ input amount, adjusted for token decimals).
Build and Send Transaction Build a compressed v0 transaction with lookup tables and send it. await new Promise (( resolve ) => setTimeout ( resolve , 2000 ));
const { value : latestBlockhash } = await rpc
. getLatestBlockhash ({ commitment: 'finalized' })
. send ();
// Prepare LUT addresses for compression
const lutsByAddress : Record < Address , Address []> = {};
const bestRouteLutAccounts = await fetchAllAddressLookupTable ( rpc , bestRoute . luts || []);
for ( const acc of bestRouteLutAccounts ) {
lutsByAddress [ acc . address ] = acc . data . addresses ;
}
// Build transaction with LUT compression
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 );
console . log ( `Position: ~ ${ depositAmount . mul ( leverage ) } JLP with ${ depositAmount . mul ( leverage - 1 ) } USDC debt` );
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 } from '@kamino-finance/kswap-sdk/dist' ;
import type { RouteOutput , RouteParams , RouterType } from '@kamino-finance/kswap-sdk/dist' ;
import Decimal from 'decimal.js' ;
import type { Address } from '@solana/kit' ;
import { toLegacyPublicKey } from '@kamino-finance/klend-sdk/dist/utils/compat' ;
import { fromLegacyInstructions , fromLegacyLookupTables } from './compat' ;
const ALLOWED_ROUTERS : RouterType [] = [ 'dflow' , 'jupiter' , 'jupiterU' , 'okx' , 'jupiterLite' ];
export async function getTokenPriceFromJupWithFallback (
kswapSdk : KswapSdk ,
inputMint : Address ,
outputMint : Address
) : Promise < number > {
const params = {
ids: inputMint . toString (),
vsToken: outputMint . toString (),
};
const res = await kswapSdk . getJupiterPriceWithFallback ( params );
return Number ( res [ inputMint . toString ()]?. usdPrice || 0 );
}
export function getKswapQuoter (
kswapSdk : KswapSdk ,
executor : Address ,
slippageBps : number ,
inputMintReserve : KaminoReserve ,
outputMintReserve : KaminoReserve
) : SwapQuoteProvider < RouteOutput > {
return async ( inputs : SwapInputs ) : Promise < SwapQuote < RouteOutput >> => {
const routeParams : RouteParams = {
executor: toLegacyPublicKey ( executor ),
tokenIn: toLegacyPublicKey ( inputs . inputMint ),
tokenOut: toLegacyPublicKey ( inputs . outputMint ),
amount: new BN ( inputs . inputAmountLamports . toDP ( 0 ). toString ()),
maxSlippageBps: slippageBps ,
wrapAndUnwrapSol: false ,
swapType: 'exactIn' ,
routerTypes: ALLOWED_ROUTERS ,
includeRfq: false ,
includeLimoLogs: false ,
};
const routeOutputs = await kswapSdk . getAllRoutes ( routeParams );
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 inAmount = new Decimal ( bestRoute . amountsExactIn . amountIn . toString ())
. div ( inputMintReserve . getMintFactor ());
const minAmountOut = new Decimal ( bestRoute . amountsExactIn . amountOutGuaranteed . toString ())
. div ( outputMintReserve . getMintFactor ());
const priceAInB = minAmountOut . div ( inAmount );
return {
priceAInB ,
quoteResponse: bestRoute ,
};
};
}
export function getKswapSwapper (
kswapSdk : KswapSdk ,
executor : Address ,
slippageBps : number
) : SwapIxsProvider < RouteOutput > {
return async ( inputs : SwapInputs , klendAccounts : Address [], quote : SwapQuote < RouteOutput >) : Promise < SwapIxs < RouteOutput >[]> => {
const routeParams : RouteParams = {
executor: toLegacyPublicKey ( executor ),
tokenIn: toLegacyPublicKey ( inputs . inputMint ),
tokenOut: toLegacyPublicKey ( inputs . outputMint ),
amount: new BN ( inputs . inputAmountLamports . toString ()),
maxSlippageBps: slippageBps ,
wrapAndUnwrapSol: false ,
swapType: 'exactIn' ,
routerTypes: ALLOWED_ROUTERS ,
includeRfq: false ,
includeLimoLogs: false ,
};
const routeOutputs = await kswapSdk . getAllRoutes ( routeParams );
return routeOutputs . routes . map (( routeOutput ) => {
const inAmount = new Decimal ( routeOutput . amountsExactIn . amountIn . toString ())
. div ( routeOutput . inputTokenDecimals ! );
const minAmountOut = new Decimal ( routeOutput . amountsExactIn . amountOutGuaranteed . toString ())
. div ( routeOutput . outputTokenDecimals ! );
const priceAInB = minAmountOut . div ( inAmount );
return {
preActionIxs: [],
swapIxs: fromLegacyInstructions ( ... routeOutput . ixsRouter ! ),
lookupTables: fromLegacyLookupTables ( ... routeOutput . lookupTableAccounts ! ),
quote: {
priceAInB ,
quoteResponse: routeOutput ,
},
};
});
};
}
import type { Account , Instruction } from '@solana/kit' ;
import { AccountRole , lamports , none , some } from '@solana/kit' ;
import { AddressLookupTableAccount , TransactionInstruction } from '@solana/web3.js' ;
import { fromLegacyPublicKey , fromLegacyTransactionInstruction } from '@solana/compat' ;
import type { AddressLookupTable } from '@solana-program/address-lookup-table' ;
import { ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS } from '@solana-program/address-lookup-table' ;
export function fromLegacyLookupTables ( ... account : AddressLookupTableAccount []) : Account < AddressLookupTable >[] {
return account . map ( fromLegacyLookupTable );
}
function fromLegacyLookupTable ( lut : AddressLookupTableAccount ) : Account < AddressLookupTable > {
return {
address: fromLegacyPublicKey ( lut . key ),
programAddress: ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS ,
executable: false ,
lamports: lamports ( 0 n ),
data: {
padding: 0 ,
lastExtendedSlotStartIndex: lut . state . lastExtendedSlotStartIndex ,
lastExtendedSlot: BigInt ( lut . state . lastExtendedSlot ),
addresses: lut . state . addresses . map ( fromLegacyPublicKey ),
authority: lut . state . authority ? some ( fromLegacyPublicKey ( lut . state . authority )) : none (),
discriminator: 0 ,
deactivationSlot: lut . state . deactivationSlot ,
},
space: 0 n ,
};
}
export function fromLegacyInstructions ( ... legacy : TransactionInstruction []) : Instruction [] {
return legacy . map ( fromLegacyTransactionInstruction );
}
export function getAccountRole ({ isSigner , isMut } : { isSigner : boolean ; isMut : boolean }) : AccountRole {
if ( isSigner && isMut ) return AccountRole . WRITABLE_SIGNER ;
if ( isSigner && ! isMut ) return AccountRole . READONLY_SIGNER ;
if ( ! isSigner && isMut ) return AccountRole . WRITABLE ;
return AccountRole . READONLY ;
}
Transaction Flow
View Transaction Flow Diagram
Asset Flow
3x Leveraged Position: Step-by-Step
Initial State
Wallet: 1 USDC
Position: None
Flash Borrow
Flash loan: 2 USDC (temporary)
Available: 3 USDC total
Swap & Deposit
Swap 3 USDC to 3 JLP via KSwap
Deposit 3 JLP as collateral
Borrow & Repay
Borrow 2 USDC against collateral
Repay 2 USDC flash loan
Final Position
Collateral: 3 JLP
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
Enables flash loan patterns : Indicated by obligation: null parameter
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 : Reduces size from ~3096 to <1232 bytes
Multiply-specific LUTs : Fetched from Kamino CDN per collateral/debt pair
Critical for complex txns : Multiply operations wouldn’t fit without LUTs
KSwap Multi-Route Optimization
KSwap provides optimal swap execution through:
Multi-DEX querying : Jupiter, Dflow, OKX, Jupiter Lite
Route simulation : Tests each route for success
Price comparison : Selects route with best effective price
Fallback logic : Uses first route if all simulations fail
Slippage protection : Guarantees minimum output amount
Solana commitment levels affect confirmation speed:
processed : Fastest, used for sending (no false timeout errors)
confirmed : Medium speed, 30+ slots of confirmation
finalized : Slowest, used for blockhash (maximum security)
Recommended : Use processed for multiply transactions to avoid false errors