TypeScript SDK

Flash Loans

How marginfi flash loans work, how the SDK uses them under the hood, and how to wrap your own instruction sequence with makeFlashLoanTx.

A marginfi flash loan lets you borrow without collateral as long as the account is healthy again by the end of the same transaction. The SDK's loop, swap and repay-with-collateral builders are all flash loans internally; this page shows how to compose one yourself. It assumes you have a client and wrappedAccount from Getting Started. For the protocol-level rules see the Flashloans guide.

How flash loans work

A flash loan is a pair of instructions that bookend a transaction:

PositionInstructionWhat it does
firstlending_account_start_flashloan (makeBeginFlashLoanIx)Flags the account as in-flashloan and records endIndex, the instruction index where the matching end must sit.
middleany instructionsBorrows, withdraws, swaps, deposits, repays, CPIs into other programs. No health check runs here.
lastlending_account_end_flashloan (makeEndFlashLoanIx)Clears the flag and runs the risk engine's health check with the oracle accounts for every bank the account holds.

If the account is unhealthy at end_flashloan, or the end instruction is missing or in the wrong slot, the whole transaction reverts. Flash loans cannot be nested, cannot be called through CPI, and have no fee.

How the SDK uses them

You rarely need to build flash loans by hand. These wrapper methods already produce a flash-loan transaction (plus any setup transactions for oracle cranks or integration refreshes):

Reach for makeFlashLoanTx when you need a sequence the SDK does not offer as a builder: a custom swap venue, a liquidation, arbitrage, or a multi-step rebalance.

Building your own

wrappedAccount.makeFlashLoanTx(params) takes your inner instructions and returns an ExtendedV0Transaction with the layout [begin, ...ixs, end]. It fills in program and marginfiAccount; you supply the rest:

Prop

Type

Under the hood it runs computeProjectedActiveBanksNoCpi over ixs — decoding the marginfi deposit/borrow/repay/withdraw instructions it finds and ignoring everything else — to know which banks will be active after your sequence, and feeds that list to makeEndFlashLoanIx as the health-check accounts. endIndex is ixs.length + 1.

Inner instructions

Build marginfi instructions with the Layer-2 wrapper methods; each returns an InstructionsWrapper ({ instructions, keys }) with the bank, token program and metadata looked up for you:

MethodSignature
makeDepositIx(bankAddress, amount, opts?: MakeDepositIxOpts)
makeBorrowIx(bankAddress, amount, opts?: MakeBorrowIxOpts)
makeRepayIx(bankAddress, amount, repayAll = false, opts?: MakeRepayIxOpts)
makeWithdrawIx(bankAddress, amount, withdrawAll = false, opts?: MakeWithdrawIxOpts)

Amounts are UI units (number, string or BigNumber). Any non-marginfi instruction (a DEX swap, a CPI into your own program) goes in between as-is. The lower-level wrappedAccount.makeBeginFlashLoanIx(endIndex) and makeEndFlashLoanIx(projectedActiveBanks) are exposed too if you want to assemble the message yourself.

Complete example

Borrow USDC, run an arbitrary instruction against it, and deposit the proceeds as SOL collateral, all in one flash loan:

import { ComputeBudgetProgram, TransactionInstruction } from "@solana/web3.js";
import { AssetTag, simulateBundle } from "@0dotxyz/p0-ts-sdk";

const usdcBank = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT)[0];
const solBank = client.getBanksByMint(SOL_MINT, AssetTag.DEFAULT)[0];

// 1. Marginfi legs via the wrapper (UI units)
const borrowIxs = await wrappedAccount.makeBorrowIx(usdcBank.address, "100");
const depositIxs = await wrappedAccount.makeDepositIx(solBank.address, "0.5");

// 2. Whatever happens in between — a swap, a CPI, ... (placeholder)
const yourIxs: TransactionInstruction[] = await buildYourSwapIxs({
  from: USDC_MINT,
  to: SOL_MINT,
  amountUi: 100,
  owner: wallet.publicKey,
});

// 3. Wrap: [begin, cu, borrow, ...yourIxs, deposit, end]
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
const flashLoanTx = await wrappedAccount.makeFlashLoanTx({
  ixs: [
    ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
    ...borrowIxs.instructions,
    ...yourIxs,
    ...depositIxs.instructions,
  ],
  bankMap: client.bankMap,
  blockhash,
  addressLookupTableAccounts: [...client.addressLookupTables, ...yourLuts],
});

// 4. Simulate, sign, send
const [sim] = await simulateBundle(connection.rpcEndpoint, [flashLoanTx]);
if (sim.err) throw new Error(`flash loan failed: ${JSON.stringify(sim.err)}\n${sim.logs?.join("\n")}`);

const signed = await wallet.signTransaction(flashLoanTx);
const signature = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight });

The deposit amount here is fixed up front. The SDK's own loop builder instead sizes the deposit from the swap's guaranteed min-out after the route is known; if you need the same, compute your expected output before building depositIxs.

Compute-budget instructions belong inside ixs — the SDK's own builders put them first. Do not prepend them outside the flash loan, since begin_flashloan must be the first instruction and endIndex is computed from ixs.length.

Constraints and gotchas

  • One authority. Every inner marginfi instruction must act on wrappedAccount's account and be signed by its authority; the transaction's fee payer is that authority.
  • End healthy. Whatever happens in the middle, the account must pass the initial-margin health check at end_flashloan. Use simulateHealthCache on the expected end state if you want to pre-check.
  • Stale oracles. makeFlashLoanTx does not crank oracles for you. If a bank the account holds has a stale Switchboard feed the end health check fails; the Layer-4 builders handle this by emitting crank transactions, and you can do the same — see Transactions.
  • Account flag. wrappedAccount.isFlashLoanEnabled reflects the on-chain ACCOUNT_IN_FLASHLOAN flag; it is only set between begin and end, so a true read outside a transaction means a previous flash loan was left in a bad state.
  • Transaction size. A v0 transaction is capped at 1232 bytes and 64 account locks; begin/end plus the health-check accounts eat into that. Keep LUTs comprehensive. If you are splicing a swap and want the exact remaining budget before quoting, computeFlashLoanNonSwapBudget({ program, marginfiAccount, ixs, bankMap, addressLookupTableAccounts }) returns { sizeConstraint, maxSwapTotalAccounts }, and compileFlashloanPrecheck(...) compiles the full message and reports fullTxSize / overshoot before you build.
  • Program 0.1.10. The end_flashloan account layout changed in program 0.1.10; makeEndFlashLoanIx (and therefore makeFlashLoanTx) builds the right layout for the live program, see Program Upgrade 0.1.10.

On this page