import { Address, Instruction, Slot, Option, none, TransactionSigner, lamports } from '@solana/kit'; import Decimal from 'decimal.js'; import { KaminoAction, KaminoMarket, KaminoObligation, KaminoReserve, lamportsToNumberDecimal as fromLamports, getTokenIdsForScopeRefresh, isKaminoObligation, toJson, } from '../classes'; import { getFlashLoanInstructions } from './instructions'; import { assertAdjustDecreaseCollFlashCalcs, assertAdjustDecreaseDebtFlashCalcs, assertAdjustIncreaseCollFlashCalcs, assertAdjustIncreaseDebtFlashCalcs, assertDepositCollFlashCalcs, assertDepositDebtFlashCalcs, assertWithdrawCollFlashCalcs, assertWithdrawDebtFlashCalcs, } from './operationGuards'; import { numberToLamportsDecimal as toLamports } from '../classes'; import { LeverageObligation, MultiplyObligation, ObligationType, ObligationTypeTag, SOL_DECIMALS, ScopePriceRefreshConfig, U64_MAX, createAtasIdempotent, getAssociatedTokenAddress, getComputeBudgetAndPriorityFeeIxs, getTransferWsolIxs, KlendAccountsResult, removeBudgetIxs, toKlendAccountsResult, uniqueAccountsWithProgramIds, WRAPPED_SOL_MINT, MultiplyObligationFixedRate, LeverageObligationFixedRate, } from '../utils'; import { adjustDepositLeverageCalcs, adjustDepositLeverageCalcsDebtFlash, adjustWithdrawLeverageCalcs, adjustWithdrawLeverageCalcsCollFlash, calcAdjustAmounts, calcCollFlashLegLamports, depositLeverageCalcs, depositLeverageCalcsDebtFlash, withdrawLeverageCalcs, withdrawLeverageCalcsCollFlash, } from './calcs'; import { assertPositiveFiniteDecimal } from '../lending_operations/swap_calcs'; import { calcFlashLoanFees } from '../lending_operations/repay_with_collateral_calcs'; import { FullBPS } from '@kamino-finance/kliquidity-sdk/dist/utils/CreationParameters'; import { AdjustDepositDebtFlashCalcsResult, AdjustLeverageCalcsResult, AdjustLeverageInitialInputs, AdjustLeverageIxsParams, AdjustLeverageIxsResponse, AdjustLeverageSwapInputsParams, AdjustWithdrawCollFlashCalcsResult, DepositLeverageCalcsResult, DepositLeverageDebtFlashCalcsResult, DepositLeverageInitialInputs, DepositWithLeverageParams, DepositWithLeverageSwapInputsParams, DepositLeverageIxsResponse, SwapInputs, SwapIxs, SwapIxsProvider, SwapQuoteProvider, WithdrawLeverageCalcsResult, WithdrawLeverageCollFlashCalcsResult, WithdrawLeverageInitialInputs, WithdrawLeverageIxsResponse, WithdrawWithLeverageParams, WithdrawWithLeverageSwapInputsParams, LeverageIxsOutput, FlashLoanInfo, } from './types'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; import { findAssociatedTokenPda, getCloseAccountInstruction } from '@solana-program/token-2022'; import { LAMPORTS_PER_SOL } from '../utils/consts'; import { DistributiveOmit, requireMatchingLedgerInstant, resolveLedgerInput } from '../utils/ledger'; import { redeemWithdrawAmount, sizeRedeemFundedPull } from '../lending_operations/redeem_drift'; // Offset for the withdraw slot to underestimate the exchange rate. This is the older, // adjust-path-only mitigation for the same estimated-vs-actual redeem drift addressed by // `lending_operations/redeem_drift.ts` (which covers the repay/close/migrate paths) — candidate // for unification onto one mechanism. export const WITHDRAW_SLOT_OFFSET = 150; // Sentinel used to short-circuit a `get*WithLeverageSwapInputs` run once it has handed the klend account set to // the quoter — see `captureLeverageKlendAccounts`. class KlendAccountsCaptured { constructor(public readonly klendAccounts: Array
) {} } /** * Runs a `get*WithLeverageSwapInputs` flow with a quoter that captures the klend account set the flow passes to it * and then aborts — so we learn the exact, final accounts (the value the operation itself uses) without running the * external swap. The leverage flows build their klend ixs and compute this set before ever calling the quoter, so * the capture is complete and the abort skips only the post-quote sizing/return work. */ async function captureLeverageKlendAccounts( run: (quoter: SwapQuoteProvider) => Promise ): Promise { const capturingQuoter: SwapQuoteProvider = (_inputs, klendAccounts) => Promise.reject(new KlendAccountsCaptured(klendAccounts)); try { await run(capturingQuoter); } catch (e) { if (e instanceof KlendAccountsCaptured) { return toKlendAccountsResult(e.klendAccounts); } throw e; } throw new Error('klend account discovery did not reach the quoter; cannot determine the klend accounts'); } /** * Inputs for {@link getDepositLeverageKlendAccounts}: the {@link getDepositWithLeverageSwapInputs} props minus the * quoter (and logger), since the klend account footprint is discovered without an external swap. */ export type DepositLeverageKlendAccountsInputs = DistributiveOmit< DepositWithLeverageSwapInputsParams, 'quoter' | 'logger' >; /** * Light helper: returns the exact, final set of klend accounts (and program ids) a deposit-with-leverage operation * with the same inputs would consume, plus their count, WITHOUT running the external swap. This is the same set the * operation passes to the quoter (invariant to the swap amounts), so the count is accurate and final — the FE can * use it to know how many accounts remain for the external swap within the transaction's account limit. */ export function getDepositLeverageKlendAccounts( inputs: DepositLeverageKlendAccountsInputs ): Promise { return captureLeverageKlendAccounts((quoter) => getDepositWithLeverageSwapInputs({ ...inputs, quoter })); } /** * Inputs for {@link getWithdrawLeverageKlendAccounts}: the {@link getWithdrawWithLeverageSwapInputs} props minus the * quoter (and logger). */ export type WithdrawLeverageKlendAccountsInputs = DistributiveOmit< WithdrawWithLeverageSwapInputsParams, 'quoter' | 'logger' >; /** Light helper: the accurate, final klend account footprint of a withdraw-with-leverage operation. See {@link getDepositLeverageKlendAccounts}. */ export function getWithdrawLeverageKlendAccounts( inputs: WithdrawLeverageKlendAccountsInputs ): Promise { return captureLeverageKlendAccounts((quoter) => getWithdrawWithLeverageSwapInputs({ ...inputs, quoter })); } /** * Inputs for {@link getAdjustLeverageKlendAccounts}: the {@link getAdjustLeverageSwapInputs} props minus the quoter * (and logger). */ export type AdjustLeverageKlendAccountsInputs = DistributiveOmit< AdjustLeverageSwapInputsParams, 'quoter' | 'logger' >; /** Light helper: the accurate, final klend account footprint of an adjust-leverage operation. See {@link getDepositLeverageKlendAccounts}. */ export function getAdjustLeverageKlendAccounts( inputs: AdjustLeverageKlendAccountsInputs ): Promise { return captureLeverageKlendAccounts((quoter) => getAdjustLeverageSwapInputs({ ...inputs, quoter })); } export async function getDepositWithLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, depositAmount, priceDebtToColl, slippagePct, obligation, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, targetLeverage, selectedTokenMint, obligationTypeTagOverride, scopeRefreshIx, budgetAndPriorityFeeIxs, quoteBufferBps, quoter, useV2Ixs, elevationGroupOverride, flashBorrowType, logger, }: DepositWithLeverageSwapInputsParams): Promise<{ flashLoanInfo: FlashLoanInfo; swapInputs: SwapInputs; initialInputs: DepositLeverageInitialInputs; }> { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, !debtReserve.state.config.debtMaturityTimestamp.eqn(0), 'getDepositWithLeverageSwapInputs' ); const { currentSlot, currentLedgerInstant } = ledger; const log = logger ?? (() => {}); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const solTokenReserve = collReserve.getLiquidityMint() === WRAPPED_SOL_MINT ? collReserve : debtReserve.getLiquidityMint() === WRAPPED_SOL_MINT ? debtReserve : undefined; const selectedTokenIsCollToken = selectedTokenMint === collTokenMint; const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve.getLiquidityMint(); const obligationType = checkObligationType( obligationTypeTagOverride, collReserve.address, debtReserve.address, kaminoMarket ); const dummySwapIxs: SwapIxs[] = [ { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined } }, ]; const resolvedObligation = obligation ? obligation : obligationType; if (flashBorrowType !== 'debt') { // Coll flash path (default): flash borrow coll -> deposit coll -> borrow debt -> swap debt->coll -> flash repay coll const flashLoanFee = collReserve.getFlashLoanFee(); const calcs = depositLeverageCalcs({ depositAmount: depositAmount, depositTokenIsCollToken: selectedTokenIsCollToken, depositTokenIsSol, priceDebtToColl, targetLeverage, slippagePct, flashLoanFee, }); log('Deposit calcs (coll flash)', toJson(calcs)); const klendIxs: LeverageIxsOutput = ( await buildDepositWithLeverageIxsCollFlash( kaminoMarket, debtReserve, collReserve, owner, resolvedObligation, referrer, currentSlot, depositTokenIsSol, scopeRefreshIx, calcs, budgetAndPriorityFeeIxs, dummySwapIxs, useV2Ixs, elevationGroupOverride ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: debtTokenMint, outputMint: collTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const quotePriceCalcs = depositLeverageCalcs({ depositAmount: depositAmount, depositTokenIsCollToken: selectedTokenIsCollToken, depositTokenIsSol, priceDebtToColl: swapQuote.priceAInB, targetLeverage, slippagePct, flashLoanFee, }); const swapInputAmountQuotePrice = toLamports(quotePriceCalcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil(); // The coll ATA is empty after the exact-spend deposit and the flash repay debits `flashBorrow + fee` // (1-lamport minimum included), funded solely by the swap output — so the declared minimum output must be the // canonical lamport-domain debit; `flashBorrow` alone leaves a minimum-fill short by the fee. Mirrors the // builder's ceil-rounded flash borrow (`buildDepositWithLeverageIxsCollFlash`). const depositFlashBorrowLamports = toLamports( quotePriceCalcs.flashBorrowInCollToken, collReserve.stats.decimals ).ceil(); const depositMinCollOutLamports = calcFlashLoanFees({ reserve: collReserve, referralFeeBps: 0, hasReferral: false, flashBorrowAmountLamports: depositFlashBorrowLamports, }).flashRepayDebitLamports.ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, minOutAmountLamports: depositMinCollOutLamports, inputMint: debtTokenMint, outputMint: collTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: quotePriceCalcs, swapQuote, currentSlot, currentLedgerInstant, obligation: resolvedObligation, klendAccounts: uniqueKlendAccounts, }, }; } else { // Debt flash path: flash borrow debt -> swap debt->coll -> deposit coll -> borrow debt -> flash repay debt const flashLoanFee = debtReserve.getFlashLoanFee(); const calcs = depositLeverageCalcsDebtFlash({ depositAmount, depositTokenIsCollToken: selectedTokenIsCollToken, depositTokenIsSol, priceDebtToColl, targetLeverage, slippagePct, flashLoanFee, }); log('Deposit calcs (debt flash)', toJson(calcs)); const klendIxs: LeverageIxsOutput = ( await buildDepositWithLeverageIxsDebtFlash( kaminoMarket, debtReserve, collReserve, owner, resolvedObligation, referrer, currentSlot, depositTokenIsSol, scopeRefreshIx, calcs, budgetAndPriorityFeeIxs, dummySwapIxs, useV2Ixs, elevationGroupOverride ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: debtTokenMint, outputMint: collTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const quotePriceCalcs = depositLeverageCalcsDebtFlash({ depositAmount, depositTokenIsCollToken: selectedTokenIsCollToken, depositTokenIsSol, priceDebtToColl: swapQuote.priceAInB, targetLeverage, slippagePct, flashLoanFee, }); const swapInputAmountQuotePrice = toLamports(quotePriceCalcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, minOutAmountLamports: toLamports(quotePriceCalcs.swapCollTokenExpectedOut, collReserve.stats.decimals), inputMint: debtTokenMint, outputMint: collTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: quotePriceCalcs, swapQuote, currentSlot, currentLedgerInstant, obligation: resolvedObligation, klendAccounts: uniqueKlendAccounts, }, }; } } export async function getDepositWithLeverageIxs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, depositAmount, priceDebtToColl, slippagePct, obligation, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, targetLeverage, selectedTokenMint, obligationTypeTagOverride, scopeRefreshIx, budgetAndPriorityFeeIxs, quoteBufferBps, quoter, swapper, elevationGroupOverride, useV2Ixs, rollOver, flashBorrowType, logger, }: DepositWithLeverageParams): Promise>> { const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, !debtReserve.state.config.debtMaturityTimestamp.eqn(0), 'getDepositWithLeverageIxs' ); const { currentSlot, currentLedgerInstant } = ledger; const { swapInputs, initialInputs } = await getDepositWithLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, depositAmount, priceDebtToColl, slippagePct, obligation, referrer, currentSlot, currentLedgerInstant, targetLeverage, selectedTokenMint, obligationTypeTagOverride, scopeRefreshIx, budgetAndPriorityFeeIxs, quoteBufferBps, quoter, useV2Ixs, flashBorrowType, logger, }); const depositSwapper: SwapIxsProvider = swapper; const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote); // Strategy lookup table logic removed const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); // Leverage deposit borrows debt; reject up front if the debt is a fixed-term reserve past its maturity (the // on-chain borrow would revert with ReserveDebtMaturityReached). if (!debtReserve.state.config.debtMaturityTimestamp.eqn(0)) { debtReserve.assertCanOriginateDebt( Number(requireMatchingLedgerInstant(currentSlot, currentLedgerInstant, 'getDepositWithLeverageIxs').blockTime) ); } const solTokenReserve = collReserve.getLiquidityMint() === WRAPPED_SOL_MINT ? collReserve : debtReserve.getLiquidityMint() === WRAPPED_SOL_MINT ? debtReserve : undefined; const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve!.getLiquidityMint(); const swapIxsArray = swapsArray.map((swap) => { return { preActionIxs: [] as Instruction[], swapIxs: swap.swapIxs, lookupTables: swap.lookupTables, quote: swap.quote, }; }); let depositWithLeverageIxs: LeverageIxsOutput[]; const depositCalcs = initialInputs.calcs; if (flashBorrowType !== 'debt') { // Coll flash path (default): flash borrow coll -> deposit+borrow -> swap debt->coll -> flash repay coll assertDepositCollFlashCalcs(depositCalcs); depositWithLeverageIxs = await buildDepositWithLeverageIxsCollFlash( kaminoMarket, debtReserve!, collReserve!, owner, initialInputs.obligation, referrer, currentSlot, depositTokenIsSol, scopeRefreshIx, depositCalcs, budgetAndPriorityFeeIxs, swapIxsArray, useV2Ixs, elevationGroupOverride, rollOver ); } else { // Debt flash path: flash borrow debt -> swap debt->coll -> deposit+borrow -> flash repay debt assertDepositDebtFlashCalcs(depositCalcs); depositWithLeverageIxs = await buildDepositWithLeverageIxsDebtFlash( kaminoMarket, debtReserve!, collReserve!, owner, initialInputs.obligation, referrer, currentSlot, depositTokenIsSol, scopeRefreshIx, depositCalcs, budgetAndPriorityFeeIxs, swapIxsArray, useV2Ixs, elevationGroupOverride ); } // Depositing leverage borrows debt; if the debt reserve is fixed-rate the borrow (re)stamps a fresh term/maturity. const reorigination = debtReserve.getFixedTermReorigination(); return depositWithLeverageIxs.map((depositWithLeverageIxs, index) => { return { ixs: depositWithLeverageIxs.instructions, flashLoanInfo: depositWithLeverageIxs.flashLoanInfo, lookupTables: swapsArray[index].lookupTables, swapInputs, initialInputs, quote: swapsArray[index].quote.quoteResponse, reorigination, }; }); } async function buildDepositWithLeverageIxsCollFlash( market: KaminoMarket, debtReserve: KaminoReserve, collReserve: KaminoReserve, owner: TransactionSigner, obligation: KaminoObligation | ObligationType | undefined, referrer: Option
, currentSlot: Slot, depositTokenIsSol: boolean, scopeRefreshIx: Instruction[], calcs: DepositLeverageCalcsResult, budgetAndPriorityFeeIxs: Instruction[] | undefined, swapQuoteIxsArray: SwapIxs[], useV2Ixs: boolean, elevationGroupOverride?: number, rollOver?: boolean ): Promise { const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [[collTokenAta]] = await Promise.all([ findAssociatedTokenPda({ owner: owner.address, mint: collTokenMint, tokenProgram: collReserve.getLiquidityTokenProgram(), }), ]); // 1. Create atas & budget ixs const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); const fillWsolAtaIxs: Instruction[] = []; if (depositTokenIsSol) { fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address), lamports(BigInt(toLamports(calcs.initDepositInSol, SOL_DECIMALS).ceil().toString())) ) ); } // 2. Flash borrow & repay the collateral amount needed for given leverage // if user deposits coll, then we borrow the diff, else we borrow the entire amount const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await market.getLendingMarketAuthority(), lendingMarketAddress: market.getAddress(), reserve: collReserve, // Ceil: flash-borrow integer lamports at the call site (the swap/borrow funding leg covers the SC fee). Flooring // would under-borrow the deposit bridge by up to 1 lamport. amountLamports: toLamports(calcs.flashBorrowInCollToken, collReserve.stats.decimals).ceil(), destinationAta: collTokenAta, // TODO(referrals): once we support referrals, we will have to replace the placeholder args below: referrerAccount: none(), referrerTokenState: none(), programId: market.programId, }); // 3. Deposit initial tokens + borrowed tokens into reserve const kaminoDepositAndBorrowAction = await KaminoAction.buildDepositAndBorrowTxns({ kaminoMarket: market, depositAmount: toLamports(calcs.collTokenToDeposit, collReserve.stats.decimals).floor().toString(), depositReserveAddress: collReserve.address, borrowAmount: toLamports(calcs.debtTokenToBorrow, debtReserve.stats.decimals).ceil().toString(), borrowReserveAddress: debtReserve.address, owner, obligation: obligation!, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: elevationGroupOverride === 0 ? false : true, // emode initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI referrer, currentSlot, rollOver, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { // 4. Swap const { swapIxs } = swapQuoteIxs; const swapInstructions = removeBudgetIxs(swapIxs); const flashBorrowReserve = collReserve; const flashLoanInfo = { flashBorrowReserve: flashBorrowReserve.address, flashLoanFee: flashBorrowReserve.getFlashLoanFee(), }; return { flashLoanInfo, instructions: [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...KaminoAction.actionToIxs(kaminoDepositAndBorrowAction), ...swapInstructions, ...[flashRepayIx], ...budgetIxs, ], }; }); } /** * Deposit with flash borrow DEBT token. * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(DEBT) → swap(debt→coll) → deposit+borrow → flashRepay(DEBT) → budget */ async function buildDepositWithLeverageIxsDebtFlash( market: KaminoMarket, debtReserve: KaminoReserve, collReserve: KaminoReserve, owner: TransactionSigner, obligation: KaminoObligation | ObligationType | undefined, referrer: Option
, currentSlot: Slot, depositTokenIsSol: boolean, scopeRefreshIx: Instruction[], calcs: DepositLeverageDebtFlashCalcsResult, budgetAndPriorityFeeIxs: Instruction[] | undefined, swapQuoteIxsArray: SwapIxs[], useV2Ixs: boolean, elevationGroupOverride?: number ): Promise { const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [debtTokenAta] = await findAssociatedTokenPda({ owner: owner.address, mint: debtTokenMint, tokenProgram: debtReserve.getLiquidityTokenProgram(), }); // 1. Create atas & budget ixs const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); const fillWsolAtaIxs: Instruction[] = []; if (depositTokenIsSol) { fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address), lamports(BigInt(toLamports(calcs.initDepositInSol, SOL_DECIMALS).ceil().toString())) ) ); } // 2. Flash borrow DEBT token = the exact swap spend (`flashBorrowInDebtToken`). // Ceil: this funds the ceil-sized swap exact-in; flooring under-covers it by 1 lamport. const flashBorrowDebtLamports = toLamports(calcs.flashBorrowInDebtToken, debtReserve.stats.decimals).ceil(); // The klend borrow that repays the flash must cover `flashBorrow + SC fee` (1-lamport minimum + referrer split // honoured) — size it from the shared helper instead of hand-rolling `flashBorrow*(1+fee)`. fee==0 → borrow == flash. const debtBorrowToRepayFlashLamports = calcFlashLoanFees({ reserve: debtReserve, referralFeeBps: 0, hasReferral: false, // the flash ixs carry no referrer; the SC fee total is referral-split-independent anyway flashBorrowAmountLamports: flashBorrowDebtLamports, }).flashRepayDebitLamports.ceil(); const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await market.getLendingMarketAuthority(), lendingMarketAddress: market.getAddress(), reserve: debtReserve, amountLamports: flashBorrowDebtLamports, destinationAta: debtTokenAta, referrerAccount: none(), referrerTokenState: none(), programId: market.programId, }); // 3. Deposit coll + borrow debt const kaminoDepositAndBorrowAction = await KaminoAction.buildDepositAndBorrowTxns({ kaminoMarket: market, depositAmount: toLamports(calcs.collTokenToDeposit, collReserve.stats.decimals).floor().toString(), depositReserveAddress: collReserve.address, borrowAmount: debtBorrowToRepayFlashLamports.toString(), borrowReserveAddress: debtReserve.address, owner, obligation: obligation!, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: elevationGroupOverride === 0 ? false : true, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, currentSlot, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs); const flashBorrowReserve = debtReserve; const flashLoanInfo = { flashBorrowReserve: flashBorrowReserve.address, flashLoanFee: flashBorrowReserve.getFlashLoanFee(), }; // Key difference: swap BEFORE deposit+borrow (we need the coll from the swap to deposit) return { flashLoanInfo, instructions: [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...swapInstructions, ...KaminoAction.actionToIxs(kaminoDepositAndBorrowAction), ...[flashRepayIx], ...budgetIxs, ], }; }); } export async function getWithdrawWithLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, deposited, borrowed, obligation, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, withdrawAmount, priceCollToDebt, slippagePct, isClosingPosition, selectedTokenMint, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, useV2Ixs, userSolBalanceLamports, flashBorrowType, logger, }: WithdrawWithLeverageSwapInputsParams): Promise<{ swapInputs: SwapInputs; flashLoanInfo: FlashLoanInfo; initialInputs: WithdrawLeverageInitialInputs; }> { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, debtReserve.getKind().isFixedRate(), 'getWithdrawWithLeverageSwapInputs' ); const { currentSlot, currentLedgerInstant } = ledger; const log = logger ?? (() => {}); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const selectedTokenIsCollToken = selectedTokenMint === collTokenMint; const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT; // Closing to the debt token swaps the FULL withdrawn collateral: the exact-in is sized from the // off-chain estimate of the deposit (`deposited`), while the U64_MAX withdraw redeems the // *actual* balance at the execution slot. Haircut such a swap input so estimate drift cannot // push it above the redeem output; minOut is untouched (the slippage margin dwarfs the 1e-6 // haircut). Non-closing withdraws keep their margin via the buffered withdraw in the builders. // See `lending_operations/redeem_drift.ts`. const sizeSwapInForRedeemDrift = (swapInLamports: Decimal): Decimal => sizeRedeemFundedPull(swapInLamports, isClosingPosition && !selectedTokenIsCollToken); const dummySwapIxs: SwapIxs[] = [ { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined } }, ]; if (flashBorrowType !== 'coll') { // Debt flash path (default): flash borrow debt -> repay+withdraw -> swap coll->debt -> flash repay debt const flashLoanFee = debtReserve!.getFlashLoanFee(); const calcs = withdrawLeverageCalcs( kaminoMarket, collReserve!, debtReserve!, priceCollToDebt, withdrawAmount, deposited, borrowed, currentSlot, isClosingPosition, selectedTokenIsCollToken, selectedTokenMint, obligation, flashLoanFee, slippagePct, currentLedgerInstant ); log('Withdraw calcs (debt flash)', toJson(calcs)); const klendIxs = ( await buildWithdrawWithLeverageIxsDebtFlash( kaminoMarket, debtReserve!, collReserve!, owner, obligation, referrer, currentSlot, isClosingPosition, inputTokenIsSol, scopeRefreshIx, calcs, budgetAndPriorityFeeIxs, dummySwapIxs, useV2Ixs, userSolBalanceLamports ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve!.getMintDecimals()).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: collTokenMint, outputMint: debtTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const calcsQuotePrice = withdrawLeverageCalcs( kaminoMarket, collReserve!, debtReserve!, swapQuote.priceAInB, withdrawAmount, deposited, borrowed, currentSlot, isClosingPosition, selectedTokenIsCollToken, selectedTokenMint, obligation, flashLoanFee, slippagePct, currentLedgerInstant ); const swapInputAmountQuotePrice = toLamports( calcsQuotePrice.collTokenSwapIn, collReserve!.getMintDecimals() ).ceil(); return { swapInputs: { inputAmountLamports: sizeSwapInForRedeemDrift(swapInputAmountQuotePrice), minOutAmountLamports: calcsQuotePrice.repayFundingAmount, inputMint: collTokenMint, outputMint: debtTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, }, }; } else { // Coll flash path: flash borrow coll -> swap coll->debt -> repay+withdraw -> flash repay coll const flashLoanFee = collReserve!.getFlashLoanFee(); const calcs = withdrawLeverageCalcsCollFlash( kaminoMarket, collReserve!, debtReserve!, priceCollToDebt, withdrawAmount, deposited, borrowed, currentSlot, isClosingPosition, selectedTokenIsCollToken, selectedTokenMint, obligation, flashLoanFee, slippagePct, currentLedgerInstant ); log('Withdraw calcs (coll flash)', toJson(calcs)); const klendIxs = ( await buildWithdrawWithLeverageIxsCollFlash( kaminoMarket, debtReserve!, collReserve!, owner, obligation, referrer, currentSlot, isClosingPosition, inputTokenIsSol, scopeRefreshIx, calcs, budgetAndPriorityFeeIxs, dummySwapIxs, useV2Ixs, userSolBalanceLamports ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve!.getMintDecimals()).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: collTokenMint, outputMint: debtTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const calcsQuotePrice = withdrawLeverageCalcsCollFlash( kaminoMarket, collReserve!, debtReserve!, swapQuote.priceAInB, withdrawAmount, deposited, borrowed, currentSlot, isClosingPosition, selectedTokenIsCollToken, selectedTokenMint, obligation, flashLoanFee, slippagePct, currentLedgerInstant ); const swapInputAmountQuotePrice = toLamports( calcsQuotePrice.collTokenSwapIn, collReserve!.getMintDecimals() ).ceil(); return { swapInputs: { inputAmountLamports: sizeSwapInForRedeemDrift(swapInputAmountQuotePrice), minOutAmountLamports: calcsQuotePrice.repayFundingAmount, inputMint: collTokenMint, outputMint: debtTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, }, }; } } export async function getWithdrawWithLeverageIxs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, obligation, deposited, borrowed, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, withdrawAmount, priceCollToDebt, slippagePct, isClosingPosition, selectedTokenMint, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, swapper, useV2Ixs, userSolBalanceLamports, flashBorrowType, }: WithdrawWithLeverageParams): Promise>> { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, debtReserve.getKind().isFixedRate(), 'getWithdrawWithLeverageIxs' ); const { currentSlot, currentLedgerInstant } = ledger; const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT; const { swapInputs, initialInputs } = await getWithdrawWithLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, deposited, borrowed, obligation, referrer, currentSlot, currentLedgerInstant, withdrawAmount, priceCollToDebt, slippagePct, isClosingPosition, selectedTokenMint, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, useV2Ixs, userSolBalanceLamports, flashBorrowType, }); const withdrawSwapper: SwapIxsProvider = swapper; const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote); // Strategy lookup table logic removed const swapIxsArray = swapsArray.map((swap) => { return { preActionIxs: [] as Instruction[], swapIxs: swap.swapIxs, lookupTables: swap.lookupTables, quote: swap.quote, }; }); let withdrawWithLeverageIxs: LeverageIxsOutput[]; const withdrawCalcs = initialInputs.calcs; if (flashBorrowType !== 'coll') { // Debt flash path (default): flash borrow debt -> repay+withdraw -> swap coll->debt -> flash repay debt assertWithdrawDebtFlashCalcs(withdrawCalcs); withdrawWithLeverageIxs = await buildWithdrawWithLeverageIxsDebtFlash( kaminoMarket, debtReserve!, collReserve!, owner, obligation, referrer, currentSlot, isClosingPosition, inputTokenIsSol, scopeRefreshIx, withdrawCalcs, budgetAndPriorityFeeIxs, swapIxsArray, useV2Ixs, userSolBalanceLamports ); } else { // Coll flash path: flash borrow coll -> swap coll->debt -> repay+withdraw -> flash repay coll assertWithdrawCollFlashCalcs(withdrawCalcs); withdrawWithLeverageIxs = await buildWithdrawWithLeverageIxsCollFlash( kaminoMarket, debtReserve!, collReserve!, owner, obligation, referrer, currentSlot, isClosingPosition, inputTokenIsSol, scopeRefreshIx, withdrawCalcs, budgetAndPriorityFeeIxs, swapIxsArray, useV2Ixs, userSolBalanceLamports ); } // Send ixs and lookup tables return withdrawWithLeverageIxs.map((ixs, index) => { return { ixs: ixs.instructions, flashLoanInfo: ixs.flashLoanInfo, lookupTables: swapsArray[index].lookupTables, swapInputs, initialInputs: initialInputs, quote: swapsArray[index].quote.quoteResponse, }; }); } export async function buildWithdrawWithLeverageIxsDebtFlash( market: KaminoMarket, debtReserve: KaminoReserve, collReserve: KaminoReserve, owner: TransactionSigner, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, isClosingPosition: boolean, depositTokenIsSol: boolean, scopeRefreshIx: Instruction[], calcs: WithdrawLeverageCalcsResult, budgetAndPriorityFeeIxs: Instruction[] | undefined, swapQuoteIxsArray: SwapIxs[], useV2Ixs: boolean, userSolBalanceLamports: number ): Promise { const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const debtTokenAta = await getAssociatedTokenAddress( debtTokenMint, owner.address, debtReserve.getLiquidityTokenProgram() ); // 1. Create atas & budget txns & user metadata const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); const closeWsolAtaIxs: Instruction[] = []; if (depositTokenIsSol || debtTokenMint === WRAPPED_SOL_MINT) { const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address); closeWsolAtaIxs.push( getCloseAccountInstruction( { owner, destination: owner.address, account: wsolAta, }, { programAddress: TOKEN_PROGRAM_ADDRESS } ) ); } // TODO: Mihai/Marius check if we can improve this logic and not convert any SOL // This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough const fillWsolAtaIxs: Instruction[] = []; if (debtTokenMint === WRAPPED_SOL_MINT) { const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2; const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1; fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address), lamports(BigInt(toLamports(balanceToWrap, SOL_DECIMALS).ceil().toString())) ) ); } // 2. Prepare the flash borrow and flash repay amounts and ixs // We borrow exactly how much we need to repay // and repay that + flash amount fee const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await market.getLendingMarketAuthority(), lendingMarketAddress: market.getAddress(), reserve: debtReserve!, // Flash-borrow the funding amount (principal + fixed-term early-repay penalty) so the on-chain repay debit // (`repay + penalty`) is covered; the repay instruction below uses the principal only. amountLamports: toLamports(calcs.repayFundingAmount, debtReserve!.stats.decimals).ceil(), destinationAta: debtTokenAta, referrerAccount: none(), referrerTokenState: none(), programId: market.programId, }); // 3. Repay borrowed tokens and Withdraw tokens from reserve that will be swapped to repay flash loan const repayAndWithdrawAction = await KaminoAction.buildRepayAndWithdrawTxns({ kaminoMarket: market, repayAmount: isClosingPosition ? U64_MAX : toLamports(calcs.repayAmount, debtReserve.stats.decimals).floor().toString(), repayReserveAddress: debtReserve.address, // Buffered (non-close) so the redeem covers the exact-in swap it funds despite exchange-rate // drift — see `lending_operations/redeem_drift.ts`. No cap needed: `depositTokenWithdrawAmount` // is a partial slice strictly below the deposit, and near-total withdraws route as closes. withdrawAmount: redeemWithdrawAmount( toLamports(calcs.depositTokenWithdrawAmount, collReserve!.stats.decimals), isClosingPosition ), withdrawReserveAddress: collReserve.address, payer: owner, currentSlot, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI (won't be the case for withdraw anyway as this would be created in deposit) referrer, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs); return { flashLoanInfo: { flashLoanFee: debtReserve.getFlashLoanFee(), flashBorrowReserve: debtReserve.address, }, instructions: [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...KaminoAction.actionToIxs(repayAndWithdrawAction), ...swapInstructions, ...[flashRepayIx], ...closeWsolAtaIxs, ...budgetIxs, ], }; }); } /** * Withdraw with flash borrow COLLATERAL token. * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(COLL) → swap(coll→debt) → repay+withdraw → flashRepay(COLL) → closeWsol → budget */ async function buildWithdrawWithLeverageIxsCollFlash( market: KaminoMarket, debtReserve: KaminoReserve, collReserve: KaminoReserve, owner: TransactionSigner, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, isClosingPosition: boolean, depositTokenIsSol: boolean, scopeRefreshIx: Instruction[], calcs: WithdrawLeverageCollFlashCalcsResult, budgetAndPriorityFeeIxs: Instruction[] | undefined, swapQuoteIxsArray: SwapIxs[], useV2Ixs: boolean, userSolBalanceLamports: number ): Promise { const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [collTokenAta] = await findAssociatedTokenPda({ owner: owner.address, mint: collTokenMint, tokenProgram: collReserve.getLiquidityTokenProgram(), }); // 1. Create atas & budget ixs const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); const closeWsolAtaIxs: Instruction[] = []; if (depositTokenIsSol || collTokenMint === WRAPPED_SOL_MINT) { const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address); closeWsolAtaIxs.push( getCloseAccountInstruction( { owner, destination: owner.address, account: wsolAta, }, { programAddress: TOKEN_PROGRAM_ADDRESS } ) ); } const fillWsolAtaIxs: Instruction[] = []; if (collTokenMint === WRAPPED_SOL_MINT) { const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2; const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1; fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address), lamports(BigInt(toLamports(balanceToWrap, SOL_DECIMALS).ceil().toString())) ) ); } // 2. Flash borrow COLL token = the exact swap spend (`flashBorrowInCollToken`), sized by the canonical // `calcCollFlashLegLamports` (shared with the flash-borrow-type selector so viability and execution agree): // ceil-rounded borrow (flooring under-covers the exact-in swap by 1 lamport) + the SC flash fee (1-lamport // minimum, no referrer) the withdraw leg must fund. const collFlashLeg = calcCollFlashLegLamports({ collReserve, flashBorrowCollTokens: calcs.flashBorrowInCollToken, redeemBaseCollTokens: calcs.depositTokenWithdrawAmount, }); const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await market.getLendingMarketAuthority(), lendingMarketAddress: market.getAddress(), reserve: collReserve, amountLamports: collFlashLeg.flashBorrowLamports, destinationAta: collTokenAta, referrerAccount: none(), referrerTokenState: none(), programId: market.programId, }); // 3. Repay debt + withdraw coll. The withdraw pull = user-net base (`depositTokenWithdrawAmount`) + the flash fee, so // the ATA holds `flashBorrow + fee` at flash-repay with no reliance on pre-existing dust (close → U64_MAX, fee unused). const repayAndWithdrawAction = await KaminoAction.buildRepayAndWithdrawTxns({ kaminoMarket: market, repayAmount: isClosingPosition ? U64_MAX : toLamports(calcs.repayAmount, debtReserve.stats.decimals).floor().toString(), repayReserveAddress: debtReserve.address, // Buffered (non-close) so the redeem covers the exact-in swap and the coll flash repay it // funds despite exchange-rate drift — see `lending_operations/redeem_drift.ts`. No cap needed: // `depositTokenWithdrawAmount` is a partial slice strictly below the deposit, and near-total // withdraws route as closes. withdrawAmount: redeemWithdrawAmount(collFlashLeg.redeemCollLamports, isClosingPosition), withdrawReserveAddress: collReserve.address, payer: owner, currentSlot, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs); // Key difference: swap BEFORE repay+withdraw (we need the debt from the swap to repay) return { flashLoanInfo: { flashLoanFee: collReserve.getFlashLoanFee(), flashBorrowReserve: collReserve.address, }, instructions: [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...swapInstructions, ...KaminoAction.actionToIxs(repayAndWithdrawAction), ...[flashRepayIx], ...closeWsolAtaIxs, ...budgetIxs, ], }; }); } export async function getAdjustLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, obligation, depositedLamports, borrowedLamports, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, targetLeverage, priceCollToDebt, priceDebtToColl, slippagePct, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports, flashBorrowType, }: AdjustLeverageSwapInputsParams): Promise<{ swapInputs: SwapInputs; flashLoanInfo: FlashLoanInfo; initialInputs: AdjustLeverageInitialInputs; }> { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, debtReserve.getKind().isFixedRate() || !debtReserve.state.config.debtMaturityTimestamp.eqn(0), 'getAdjustLeverageSwapInputs' ); const { currentSlot, currentLedgerInstant } = ledger; const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const deposited = fromLamports(depositedLamports, collReserve.stats.decimals); const borrowed = fromLamports(borrowedLamports, debtReserve.stats.decimals); // Getting current flash loan fee from the reserve we will flash borrow from const currentLeverage = obligation.refreshedStats.leverage; const isDepositViaLeverage = targetLeverage.gte(new Decimal(currentLeverage)); // Determine effective flash borrow type: default is coll for increase, debt for decrease const effectiveFlashBorrowType = flashBorrowType || (isDepositViaLeverage ? 'coll' : 'debt'); const flashLoanFee = effectiveFlashBorrowType === 'coll' ? collReserve.getFlashLoanFee() : debtReserve.getFlashLoanFee(); const { adjustDepositPosition, adjustBorrowPosition } = calcAdjustAmounts({ currentDepositPosition: deposited, currentBorrowPosition: borrowed, targetLeverage: targetLeverage, priceCollToDebt: priceCollToDebt, flashLoanFee: new Decimal(flashLoanFee), }); const isDeposit = adjustDepositPosition.gte(0) && adjustBorrowPosition.gte(0); if (isDepositViaLeverage !== isDeposit) { throw new Error('Invalid target leverage'); } if (isDeposit) { const dummySwapIxs: SwapIxs[] = [ { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined }, }, ]; if (effectiveFlashBorrowType !== 'debt') { // Coll flash path (default): flash borrow coll -> deposit -> borrow -> swap debt->coll -> flash repay coll const calcs = adjustDepositLeverageCalcs( debtReserve!, adjustDepositPosition, adjustBorrowPosition, priceDebtToColl, flashLoanFee, slippagePct ); const klendIxs: LeverageIxsOutput = ( await buildIncreaseLeverageIxsCollFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, calcs, scopeRefreshIx, dummySwapIxs, budgetAndPriorityFeeIxs, useV2Ixs ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.borrowAmount, debtReserve.stats.decimals).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: debtTokenMint, outputMint: collTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); // `priceAInB` is inverted (`1 / priceAInB`) into `priceCollToDebt` below; a non-positive/non-finite quote would // feed Infinity/NaN into the adjust sizing. Fail fast at the boundary. assertPositiveFiniteDecimal('adjust-leverage swap quote priceAInB', swapQuote.priceAInB); const { adjustDepositPosition: adjustDepositPositionQuotePrice, adjustBorrowPosition: adjustBorrowPositionQuotePrice, } = calcAdjustAmounts({ currentDepositPosition: deposited, currentBorrowPosition: borrowed, targetLeverage, priceCollToDebt: new Decimal(1).div(swapQuote.priceAInB), flashLoanFee: new Decimal(flashLoanFee), }); const calcsQuotePrice = adjustDepositLeverageCalcs( debtReserve, adjustDepositPositionQuotePrice, adjustBorrowPositionQuotePrice, swapQuote.priceAInB, flashLoanFee, slippagePct ); const swapInputAmountQuotePrice = toLamports(calcsQuotePrice.borrowAmount, debtReserve.getMintDecimals()).ceil(); // The coll ATA nets to zero after the flash-borrow-funded deposit and the flash repay debits // `flashBorrow + fee` (1-lamport minimum included), funded solely by the swap output — declare the canonical // lamport-domain debit as the minimum output. Mirrors the builder's floor-rounded flash borrow // (`buildIncreaseLeverageIxsCollFlash` floors to stay in lockstep with the floored deposit). const increaseFlashBorrowLamports = toLamports( calcsQuotePrice.adjustDepositPosition, collReserve.stats.decimals ).floor(); const increaseMinCollOutLamports = calcFlashLoanFees({ reserve: collReserve, referralFeeBps: 0, hasReferral: false, flashBorrowAmountLamports: increaseFlashBorrowLamports, }).flashRepayDebitLamports.ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, minOutAmountLamports: increaseMinCollOutLamports, inputMint: debtTokenMint, outputMint: collTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, isDeposit, }, }; } else { // Debt flash path: flash borrow debt -> swap debt->coll -> deposit -> borrow -> flash repay debt const calcs = adjustDepositLeverageCalcsDebtFlash( debtReserve!, adjustDepositPosition, adjustBorrowPosition, priceDebtToColl, flashLoanFee, slippagePct ); const klendIxs: LeverageIxsOutput = ( await buildIncreaseLeverageIxsDebtFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, calcs, scopeRefreshIx, dummySwapIxs, budgetAndPriorityFeeIxs, useV2Ixs ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: debtTokenMint, outputMint: collTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); // `priceAInB` is inverted (`1 / priceAInB`) into `priceCollToDebt` below; a non-positive/non-finite quote would // feed Infinity/NaN into the adjust sizing. Fail fast at the boundary. assertPositiveFiniteDecimal('adjust-leverage swap quote priceAInB', swapQuote.priceAInB); const { adjustDepositPosition: adjustDepositPositionQuotePrice, adjustBorrowPosition: adjustBorrowPositionQuotePrice, } = calcAdjustAmounts({ currentDepositPosition: deposited, currentBorrowPosition: borrowed, targetLeverage, priceCollToDebt: new Decimal(1).div(swapQuote.priceAInB), flashLoanFee: new Decimal(flashLoanFee), }); const calcsQuotePrice = adjustDepositLeverageCalcsDebtFlash( debtReserve, adjustDepositPositionQuotePrice, adjustBorrowPositionQuotePrice, swapQuote.priceAInB, flashLoanFee, slippagePct ); const swapInputAmountQuotePrice = toLamports( calcsQuotePrice.swapDebtTokenIn, debtReserve.getMintDecimals() ).ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, minOutAmountLamports: toLamports(calcsQuotePrice.adjustDepositPosition, collReserve.stats.decimals), inputMint: debtTokenMint, outputMint: collTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, isDeposit, }, }; } } else { const dummySwapIxs: SwapIxs[] = [ { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined }, }, ]; if (effectiveFlashBorrowType !== 'coll') { // Debt flash path (default): flash borrow debt -> repay -> withdraw -> swap coll->debt -> flash repay debt const calcs = adjustWithdrawLeverageCalcs( adjustDepositPosition, adjustBorrowPosition, flashLoanFee, slippagePct, obligation, debtReserve, currentSlot, currentLedgerInstant ); const klendIxs: LeverageIxsOutput = ( await buildDecreaseLeverageIxsDebtFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, calcs, scopeRefreshIx, dummySwapIxs, budgetAndPriorityFeeIxs, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports( calcs.withdrawAmountWithSlippageAndFlashLoanFee, collReserve.state.liquidity.mintDecimals.toNumber() ).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: collTokenMint, outputMint: debtTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const { adjustDepositPosition: adjustDepositPositionQuotePrice, adjustBorrowPosition: adjustBorrowPositionQuotePrice, } = calcAdjustAmounts({ currentDepositPosition: deposited, currentBorrowPosition: borrowed, targetLeverage, priceCollToDebt: swapQuote.priceAInB, flashLoanFee: new Decimal(flashLoanFee), }); const calcsQuotePrice = adjustWithdrawLeverageCalcs( adjustDepositPositionQuotePrice, adjustBorrowPositionQuotePrice, flashLoanFee, slippagePct, obligation, debtReserve, currentSlot, currentLedgerInstant ); const swapInputAmountQuotePrice = toLamports( calcsQuotePrice.withdrawAmountWithSlippageAndFlashLoanFee, collReserve.getMintDecimals() ).ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, // Swap must produce principal + early-repay penalty so the on-chain repay debit succeeds. minOutAmountLamports: toLamports(calcsQuotePrice.repayFundingAmount, debtReserve.stats.decimals), inputMint: collTokenMint, outputMint: debtTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, isDeposit, }, }; } else { // Coll flash path: flash borrow coll -> swap coll->debt -> repay -> withdraw -> flash repay coll const calcs = adjustWithdrawLeverageCalcsCollFlash( adjustDepositPosition, adjustBorrowPosition, priceCollToDebt, flashLoanFee, slippagePct, obligation, debtReserve, currentSlot, currentLedgerInstant ); const klendIxs: LeverageIxsOutput = ( await buildDecreaseLeverageIxsCollFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, calcs, scopeRefreshIx, dummySwapIxs, budgetAndPriorityFeeIxs, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports ) )[0]; const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions); const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve.getMintDecimals()).ceil(); const swapInputsForQuote: SwapInputs = { inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))), inputMint: collTokenMint, outputMint: debtTokenMint, }; const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts); const { adjustDepositPosition: adjustDepositPositionQuotePrice, adjustBorrowPosition: adjustBorrowPositionQuotePrice, } = calcAdjustAmounts({ currentDepositPosition: deposited, currentBorrowPosition: borrowed, targetLeverage, priceCollToDebt: swapQuote.priceAInB, flashLoanFee: new Decimal(flashLoanFee), }); const calcsQuotePrice = adjustWithdrawLeverageCalcsCollFlash( adjustDepositPositionQuotePrice, adjustBorrowPositionQuotePrice, swapQuote.priceAInB, flashLoanFee, slippagePct, obligation, debtReserve, currentSlot, currentLedgerInstant ); const swapInputAmountQuotePrice = toLamports( calcsQuotePrice.collTokenSwapIn, collReserve.getMintDecimals() ).ceil(); return { swapInputs: { inputAmountLamports: swapInputAmountQuotePrice, // Swap must produce principal + early-repay penalty so the on-chain repay debit succeeds. minOutAmountLamports: toLamports(calcsQuotePrice.repayFundingAmount, debtReserve.stats.decimals), inputMint: collTokenMint, outputMint: debtTokenMint, }, flashLoanInfo: klendIxs.flashLoanInfo, initialInputs: { calcs: calcsQuotePrice, swapQuote, currentSlot, currentLedgerInstant, obligation, klendAccounts: uniqueKlendAccounts, isDeposit, }, }; } } } export async function getAdjustLeverageIxs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, obligation, depositedLamports, borrowedLamports, referrer, currentSlot: suppliedCurrentSlot, currentLedgerInstant: suppliedLedgerInstant, targetLeverage, priceCollToDebt, priceDebtToColl, slippagePct, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, swapper, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports, flashBorrowType, logger, }: AdjustLeverageIxsParams): Promise>> { const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const ledger = await resolveLedgerInput( kaminoMarket.getRpc(), suppliedCurrentSlot, suppliedLedgerInstant, debtReserve.getKind().isFixedRate() || !debtReserve.state.config.debtMaturityTimestamp.eqn(0), 'getAdjustLeverageIxs' ); const { currentSlot, currentLedgerInstant } = ledger; const log = logger ?? (() => {}); const { swapInputs, initialInputs } = await getAdjustLeverageSwapInputs({ owner, kaminoMarket, debtReserveAddress, collReserveAddress, obligation, depositedLamports, borrowedLamports, referrer, currentSlot, currentLedgerInstant, targetLeverage, priceCollToDebt, priceDebtToColl, slippagePct, budgetAndPriorityFeeIxs, scopeRefreshIx, quoteBufferBps, quoter, useV2Ixs, userSolBalanceLamports, flashBorrowType, }); const effectiveFlashBorrowType = flashBorrowType || (initialInputs.isDeposit ? 'coll' : 'debt'); log(initialInputs.isDeposit ? 'Increasing leverage' : 'Decreasing leverage', toJson(initialInputs.calcs)); // leverage increased so we need to deposit and borrow more if (initialInputs.isDeposit) { // Increasing leverage borrows more debt; reject up front if the debt is a fixed-term reserve past its maturity // (the on-chain borrow would revert with ReserveDebtMaturityReached). if (!debtReserve.state.config.debtMaturityTimestamp.eqn(0)) { debtReserve.assertCanOriginateDebt( Number(requireMatchingLedgerInstant(currentSlot, currentLedgerInstant, 'getAdjustLeverageIxs').blockTime) ); } const depositSwapper: SwapIxsProvider = swapper; const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote); const swapIxsArray = swapsArray.map((swap) => { return { preActionIxs: [] as Instruction[], swapIxs: swap.swapIxs, lookupTables: swap.lookupTables, quote: swap.quote, }; }); let increaseLeverageIxs: LeverageIxsOutput[]; const increaseCalcs = initialInputs.calcs; if (effectiveFlashBorrowType !== 'debt') { // Coll flash path (default): flash borrow coll -> deposit -> borrow -> swap debt->coll -> flash repay coll assertAdjustIncreaseCollFlashCalcs(increaseCalcs); increaseLeverageIxs = await buildIncreaseLeverageIxsCollFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, increaseCalcs, scopeRefreshIx, swapIxsArray, budgetAndPriorityFeeIxs, useV2Ixs ); } else { // Debt flash path: flash borrow debt -> swap debt->coll -> deposit -> borrow -> flash repay debt assertAdjustIncreaseDebtFlashCalcs(increaseCalcs); increaseLeverageIxs = await buildIncreaseLeverageIxsDebtFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, increaseCalcs, scopeRefreshIx, swapIxsArray, budgetAndPriorityFeeIxs, useV2Ixs ); } // Increasing leverage borrows more debt; a fixed-rate debt reserve (re)stamps a fresh term/maturity on the borrow. const reorigination = kaminoMarket.getExistingReserveByAddress(debtReserveAddress).getFixedTermReorigination(); return increaseLeverageIxs.map((ixs, index) => { return { ixs: ixs.instructions, flashLoanInfo: ixs.flashLoanInfo, lookupTables: swapsArray[index].lookupTables, swapInputs, initialInputs, quote: swapsArray[index].quote.quoteResponse, reorigination, }; }); } else { const withdrawSwapper: SwapIxsProvider = swapper; // 5. Get swap ixs const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote); const swapIxsArray = swapsArray.map((swap) => { return { preActionIxs: [] as Instruction[], swapIxs: swap.swapIxs, lookupTables: swap.lookupTables, quote: swap.quote, }; }); let decreaseLeverageIxs: LeverageIxsOutput[]; const decreaseCalcs = initialInputs.calcs; if (effectiveFlashBorrowType !== 'coll') { // Debt flash path (default): flash borrow debt -> repay -> withdraw -> swap coll->debt -> flash repay debt assertAdjustDecreaseDebtFlashCalcs(decreaseCalcs); decreaseLeverageIxs = await buildDecreaseLeverageIxsDebtFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, decreaseCalcs, scopeRefreshIx, swapIxsArray, budgetAndPriorityFeeIxs, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports ); } else { // Coll flash path: flash borrow coll -> swap coll->debt -> repay -> withdraw -> flash repay coll assertAdjustDecreaseCollFlashCalcs(decreaseCalcs); decreaseLeverageIxs = await buildDecreaseLeverageIxsCollFlash( owner, kaminoMarket, collReserveAddress, debtReserveAddress, obligation, referrer, currentSlot, decreaseCalcs, scopeRefreshIx, swapIxsArray, budgetAndPriorityFeeIxs, useV2Ixs, withdrawSlotOffset, userSolBalanceLamports ); } return decreaseLeverageIxs.map((ixs, index) => { return { ixs: ixs.instructions, flashLoanInfo: ixs.flashLoanInfo, lookupTables: swapsArray[index].lookupTables, swapInputs, initialInputs, quote: swapsArray[index].quote.quoteResponse, }; }); } } function buildFlashLeverageOutputs( swapQuoteIxsArray: SwapIxs[], ixsBeforeSwap: Instruction[], ixsAfterSwap: Instruction[], budgetIxs: Instruction[], flashBorrowReserve: KaminoReserve ): LeverageIxsOutput[] { return swapQuoteIxsArray.map((swapQuoteIxs) => ({ flashLoanInfo: { flashBorrowReserve: flashBorrowReserve.address, flashLoanFee: flashBorrowReserve.getFlashLoanFee(), }, instructions: [...ixsBeforeSwap, ...removeBudgetIxs(swapQuoteIxs.swapIxs), ...ixsAfterSwap, ...budgetIxs], })); } /** * Deposit and borrow tokens if leverage increased */ async function buildIncreaseLeverageIxsCollFlash( owner: TransactionSigner, kaminoMarket: KaminoMarket, collReserveAddress: Address, debtReserveAddress: Address, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, calcs: AdjustLeverageCalcsResult, scopeRefreshIx: Instruction[], swapQuoteIxsArray: SwapIxs[], budgetAndPriorityFeeIxs: Instruction[] | undefined, useV2Ixs: boolean ): Promise { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const collTokenMint = collReserve.getLiquidityMint(); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const debtTokenMint = debtReserve.getLiquidityMint(); const collTokenAta = await getAssociatedTokenAddress( collTokenMint, owner.address, collReserve.getLiquidityTokenProgram() ); // 1. Create atas & budget txns const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); // 2. Create borrow flash loan instruction const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + scopeRefreshIx.length, // TODO: how about user metadata ixs userTransferAuthority: owner, lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(), lendingMarketAddress: kaminoMarket.getAddress(), reserve: collReserve!, // Floor (explicit): this flash borrow funds the leveraged deposit, which is itself floored below — borrow exactly // what is deposited so the two stay in lockstep. (Unlike the swap-funding flash borrows, which ceil to not // under-cover an exact-in swap, this one must equal the deposit; the SC fee is funded by the `borrowAmount` leg.) amountLamports: toLamports(calcs.adjustDepositPosition, collReserve!.stats.decimals).floor(), destinationAta: collTokenAta, // TODO(referrals): once we support referrals, we will have to replace the placeholder args below: referrerAccount: none(), referrerTokenState: none(), programId: kaminoMarket.programId, }); const depositAction = await KaminoAction.buildDepositTxns({ kaminoMarket, amount: toLamports(calcs.adjustDepositPosition, collReserve.stats.decimals).floor().toString(), reserveAddress: collReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, currentSlot, }); // 4. Borrow tokens in borrow token reserve that will be swapped to repay flash loan const borrowAction = await KaminoAction.buildBorrowTxns({ kaminoMarket, amount: toLamports(calcs.borrowAmount, debtReserve.stats.decimals).ceil().toString(), reserveAddress: debtReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit) referrer, currentSlot, }); return buildFlashLeverageOutputs( swapQuoteIxsArray, [ ...scopeRefreshIx, ...createAtasIxs, ...[flashBorrowIx], ...KaminoAction.actionToIxs(depositAction), ...KaminoAction.actionToIxs(borrowAction), ], [flashRepayIx], budgetIxs, collReserve ); } /** * Increase leverage with flash borrow DEBT token. * Order: scopeRefresh → createAtas → flashBorrow(DEBT) → swap(debt→coll) → deposit → borrow → flashRepay(DEBT) → budget */ async function buildIncreaseLeverageIxsDebtFlash( owner: TransactionSigner, kaminoMarket: KaminoMarket, collReserveAddress: Address, debtReserveAddress: Address, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, calcs: AdjustDepositDebtFlashCalcsResult, scopeRefreshIx: Instruction[], swapQuoteIxsArray: SwapIxs[], budgetAndPriorityFeeIxs: Instruction[] | undefined, useV2Ixs: boolean ): Promise { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [debtTokenAta] = await findAssociatedTokenPda({ owner: owner.address, mint: debtTokenMint, tokenProgram: debtReserve.getLiquidityTokenProgram(), }); // 1. Create atas & budget txns const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); // 2. Flash borrow DEBT = the exact swap spend (`flashBorrowInDebtToken`). // Ceil: flash-borrow integer lamports at the call site; flooring under-borrows the swap by 1 lamport. const flashBorrowDebtLamports = toLamports(calcs.flashBorrowInDebtToken, debtReserve.stats.decimals).ceil(); // The klend borrow that repays the flash must cover `flashBorrow + SC fee` (1-lamport minimum + referrer split // honoured) — size it from the shared helper instead of hand-rolling `flashBorrow*(1+fee)`. fee==0 → borrow == flash. const debtBorrowToRepayFlashLamports = calcFlashLoanFees({ reserve: debtReserve, referralFeeBps: 0, hasReferral: false, // the flash ixs carry no referrer; the SC fee total is referral-split-independent anyway flashBorrowAmountLamports: flashBorrowDebtLamports, }).flashRepayDebitLamports.ceil(); const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(), lendingMarketAddress: kaminoMarket.getAddress(), reserve: debtReserve, amountLamports: flashBorrowDebtLamports, destinationAta: debtTokenAta, referrerAccount: none(), referrerTokenState: none(), programId: kaminoMarket.programId, }); // 3. Deposit coll const depositAction = await KaminoAction.buildDepositTxns({ kaminoMarket, amount: toLamports(calcs.adjustDepositPosition, collReserve.stats.decimals).floor().toString(), reserveAddress: collReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, currentSlot, }); // 4. Borrow debt to repay flash (= flashBorrow + SC fee, computed via the shared helper above). const borrowAction = await KaminoAction.buildBorrowTxns({ kaminoMarket, amount: debtBorrowToRepayFlashLamports.toString(), reserveAddress: debtReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, currentSlot, }); return buildFlashLeverageOutputs( swapQuoteIxsArray, [...scopeRefreshIx, ...createAtasIxs, ...[flashBorrowIx]], [...KaminoAction.actionToIxs(depositAction), ...KaminoAction.actionToIxs(borrowAction), ...[flashRepayIx]], budgetIxs, debtReserve ); } /** * Withdraw and repay tokens if leverage decreased */ async function buildDecreaseLeverageIxsDebtFlash( owner: TransactionSigner, kaminoMarket: KaminoMarket, collReserveAddress: Address, debtReserveAddress: Address, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, calcs: AdjustLeverageCalcsResult, scopeRefreshIx: Instruction[], swapQuoteIxsArray: SwapIxs[], budgetAndPriorityFeeIxs: Instruction[] | undefined, useV2Ixs: boolean, withdrawSlotOffset: number = WITHDRAW_SLOT_OFFSET, userSolBalanceLamports: number ): Promise { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [debtTokenAta] = await findAssociatedTokenPda({ owner: owner.address, mint: debtTokenMint, tokenProgram: debtReserve.getLiquidityTokenProgram(), }); // 1. Create atas & budget txns const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); // TODO: Mihai/Marius check if we can improve this logic and not convert any SOL // This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough const closeWsolAtaIxs: Instruction[] = []; const fillWsolAtaIxs: Instruction[] = []; if (debtTokenMint === WRAPPED_SOL_MINT) { const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address); closeWsolAtaIxs.push( getCloseAccountInstruction( { owner, account: wsolAta, destination: owner.address, }, { programAddress: TOKEN_PROGRAM_ADDRESS } ) ); const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2; const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1; fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, wsolAta, lamports(BigInt(toLamports(balanceToWrap, debtReserve!.stats.decimals).ceil().toString())) ) ); } // 3. Flash borrow & repay amount to repay (debt) const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(), lendingMarketAddress: kaminoMarket.getAddress(), reserve: debtReserve!, // Flash-borrow the funding amount (principal + fixed-term early-repay penalty); the repay instruction below uses // the principal only. For open-term debt these coincide (penalty 0). amountLamports: toLamports(calcs.repayFundingAmount, debtReserve!.stats.decimals).ceil(), destinationAta: debtTokenAta, // TODO(referrals): once we support referrals, we will have to replace the placeholder args below: referrerAccount: none(), referrerTokenState: none(), programId: kaminoMarket.programId, }); // 4. Actually do the repay of the flash borrowed amounts const repayAction = await KaminoAction.buildRepayTxns({ kaminoMarket, amount: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals).floor().toString(), reserveAddress: debtReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, currentSlot, payer: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit) referrer, }); const withdrawSlot = currentSlot - BigInt(withdrawSlotOffset); // 6. Withdraw collateral (a little bit more to be able to pay for the slippage on swap) const withdrawAction = await KaminoAction.buildWithdrawTxns({ kaminoMarket, amount: toLamports(calcs.withdrawAmountWithSlippageAndFlashLoanFee, collReserve!.stats.decimals).ceil().toString(), reserveAddress: collReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit) referrer, currentSlot: withdrawSlot, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs); const ixs = [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...KaminoAction.actionToIxs(repayAction), ...KaminoAction.actionToIxs(withdrawAction), ...swapInstructions, ...[flashRepayIx], ...closeWsolAtaIxs, ...budgetIxs, ]; const res: LeverageIxsOutput = { flashLoanInfo: { flashBorrowReserve: debtReserve!.address, flashLoanFee: debtReserve!.getFlashLoanFee(), }, instructions: ixs, }; return res; }); } /** * Decrease leverage with flash borrow COLLATERAL token. * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(COLL) → swap(coll→debt) → repay → withdraw → flashRepay(COLL) → closeWsol → budget */ async function buildDecreaseLeverageIxsCollFlash( owner: TransactionSigner, kaminoMarket: KaminoMarket, collReserveAddress: Address, debtReserveAddress: Address, obligation: KaminoObligation, referrer: Option
, currentSlot: Slot, calcs: AdjustWithdrawCollFlashCalcsResult, scopeRefreshIx: Instruction[], swapQuoteIxsArray: SwapIxs[], budgetAndPriorityFeeIxs: Instruction[] | undefined, useV2Ixs: boolean, withdrawSlotOffset: number = WITHDRAW_SLOT_OFFSET, userSolBalanceLamports: number ): Promise { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); const [collTokenAta] = await findAssociatedTokenPda({ owner: owner.address, mint: collTokenMint, tokenProgram: collReserve.getLiquidityTokenProgram(), }); // 1. Create atas & budget txns const { budgetIxs, createAtasIxs } = await getSetupIxs( owner, collTokenMint, collReserve, debtTokenMint, debtReserve, budgetAndPriorityFeeIxs ); const closeWsolAtaIxs: Instruction[] = []; const fillWsolAtaIxs: Instruction[] = []; if (collTokenMint === WRAPPED_SOL_MINT) { const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address); closeWsolAtaIxs.push( getCloseAccountInstruction( { owner, account: wsolAta, destination: owner.address, }, { programAddress: TOKEN_PROGRAM_ADDRESS } ) ); const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2; const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1; fillWsolAtaIxs.push( ...getTransferWsolIxs( owner, wsolAta, lamports(BigInt(toLamports(balanceToWrap, collReserve.stats.decimals).ceil().toString())) ) ); } // 2. Flash borrow COLL = the exact swap spend (`flashBorrowInCollToken`), sized by the canonical // `calcCollFlashLegLamports` (shared with the flash-borrow-type selector so viability and execution agree): // ceil-rounded borrow + the SC flash fee (1-lamport minimum, no referrer) the withdraw leg below must fund // so the ATA holds `flashBorrow + fee` at flash-repay. fee==0 reserves → 0 → withdraw unchanged. const collFlashLeg = calcCollFlashLegLamports({ collReserve, flashBorrowCollTokens: calcs.flashBorrowInCollToken, redeemBaseCollTokens: calcs.depositTokenWithdrawAmount, }); const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({ borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length, userTransferAuthority: owner, lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(), lendingMarketAddress: kaminoMarket.getAddress(), reserve: collReserve, amountLamports: collFlashLeg.flashBorrowLamports, destinationAta: collTokenAta, referrerAccount: none(), referrerTokenState: none(), programId: kaminoMarket.programId, }); // 3. Repay debt const repayAction = await KaminoAction.buildRepayTxns({ kaminoMarket, amount: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve.stats.decimals).floor().toString(), reserveAddress: debtReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, currentSlot, payer: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, }); // 4. Withdraw coll = the canonical redeem (the swap spend `depositTokenWithdrawAmount` + the flash fee), so the // ATA holds `flashBorrow + fee` at flash-repay (deleverage nets nothing out). fee==0 → unchanged sizing. const withdrawSlot = currentSlot - BigInt(withdrawSlotOffset); const withdrawAction = await KaminoAction.buildWithdrawTxns({ kaminoMarket, amount: collFlashLeg.redeemCollLamports.toString(), reserveAddress: collReserve.address, owner, obligation, useV2Ixs, scopeRefreshConfig: undefined, extraComputeBudget: 0, includeAtaIxs: false, requestElevationGroup: false, initUserMetadata: { skipInitialization: true, skipLutCreation: true }, referrer, currentSlot: withdrawSlot, }); return swapQuoteIxsArray.map((swapQuoteIxs) => { const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs); // Key difference: swap BEFORE repay+withdraw const ixs = [ ...scopeRefreshIx, ...createAtasIxs, ...fillWsolAtaIxs, ...[flashBorrowIx], ...swapInstructions, ...KaminoAction.actionToIxs(repayAction), ...KaminoAction.actionToIxs(withdrawAction), ...[flashRepayIx], ...closeWsolAtaIxs, ...budgetIxs, ]; const res: LeverageIxsOutput = { flashLoanInfo: { flashBorrowReserve: collReserve.address, flashLoanFee: collReserve.getFlashLoanFee(), }, instructions: ixs, }; return res; }); } export const getSetupIxs = async ( owner: TransactionSigner, collTokenMint: Address, collReserve: KaminoReserve, debtTokenMint: Address, debtReserve: KaminoReserve, budgetAndPriorityFeeIxs: Instruction[] | undefined ) => { const budgetIxs = budgetAndPriorityFeeIxs || getComputeBudgetAndPriorityFeeIxs(3000000); const mintsWithTokenPrograms = getTokenMintsWithTokenPrograms(collTokenMint, collReserve, debtTokenMint, debtReserve); const createAtasIxs = (await createAtasIdempotent(owner, mintsWithTokenPrograms)).map((x) => x.createAtaIx); return { budgetIxs, createAtasIxs, }; }; export const getScopeRefreshIxForObligationAndReserves = async ( market: KaminoMarket, collReserve: KaminoReserve, debtReserve: KaminoReserve, obligation: KaminoObligation | ObligationType | undefined, scopeRefreshConfig: ScopePriceRefreshConfig | undefined ): Promise => { const allReserves = obligation && isKaminoObligation(obligation) ? [ ...new Set
([ ...obligation.getDeposits().map((x) => x.reserveAddress), ...obligation.getBorrows().map((x) => x.reserveAddress), collReserve.address, debtReserve.address, ]), ] : [...new Set
([collReserve.address, debtReserve.address])]; const scopeRefreshIxs: Instruction[] = []; const scopeTokensMap = getTokenIdsForScopeRefresh(market, allReserves); if (scopeTokensMap.size > 0 && scopeRefreshConfig) { for (const [configPubkey, config] of scopeRefreshConfig.scopeConfigurations) { const tokenIds = scopeTokensMap.get(config.oraclePrices); if (tokenIds && tokenIds.length > 0) { const refreshIx = await scopeRefreshConfig.scope.refreshPriceListIx({ config: configPubkey }, tokenIds); if (refreshIx) { scopeRefreshIxs.push(refreshIx); } } } } return scopeRefreshIxs; }; const checkObligationType = ( obligationTypeTag: ObligationTypeTag, collReserveAddress: Address, debtReserveAddress: Address, kaminoMarket: KaminoMarket ) => { const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress); const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress); const collTokenMint = collReserve.getLiquidityMint(); const debtTokenMint = debtReserve.getLiquidityMint(); let obligationType: ObligationType; if (obligationTypeTag === ObligationTypeTag.Multiply) { // multiply obligationType = new MultiplyObligation(collTokenMint, debtTokenMint, kaminoMarket.programId); } else if (obligationTypeTag === ObligationTypeTag.Leverage) { // leverage obligationType = new LeverageObligation(collTokenMint, debtTokenMint, kaminoMarket.programId); } else if (obligationTypeTag === ObligationTypeTag.MultiplyFixedRate) { // multiply fixed rate obligationType = new MultiplyObligationFixedRate(collReserveAddress, debtReserveAddress, kaminoMarket.programId); } else if (obligationTypeTag === ObligationTypeTag.LeverageFixedRate) { // leverage fixed rate obligationType = new LeverageObligationFixedRate(collReserveAddress, debtReserveAddress, kaminoMarket.programId); } else { throw Error('Obligation type tag not supported for leverage, please use 1 - multiply or 3 - leverage'); } return obligationType; }; type MintWithTokenProgram = { mint: Address; tokenProgram: Address; }; const getTokenMintsWithTokenPrograms = ( collTokenMint: Address, collReserve: KaminoReserve, debtTokenMint: Address, debtReserve: KaminoReserve ): Array => { return [ { mint: collTokenMint, tokenProgram: collReserve.getLiquidityTokenProgram(), }, { mint: debtTokenMint, tokenProgram: debtReserve.getLiquidityTokenProgram(), }, { mint: collReserve.getCTokenMint(), tokenProgram: TOKEN_PROGRAM_ADDRESS, }, ]; };