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:
| Position | Instruction | What it does |
|---|---|---|
| first | lending_account_start_flashloan (makeBeginFlashLoanIx) | Flags the account as in-flashloan and records endIndex, the instruction index where the matching end must sit. |
| middle | any instructions | Borrows, withdraws, swaps, deposits, repays, CPIs into other programs. No health check runs here. |
| last | lending_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):
makeLoopTx/makeBridgedLoopTx— borrow → swap → depositmakeSwapCollateralTx,makeSwapDebtTx,makeRepayWithCollatTx,makeRollPtTx— rebalance an open positionmakeTransferPositionsTx— move collateral and debt between accounts atomically
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:
| Method | Signature |
|---|---|
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. UsesimulateHealthCacheon the expected end state if you want to pre-check. - Stale oracles.
makeFlashLoanTxdoes 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.isFlashLoanEnabledreflects the on-chainACCOUNT_IN_FLASHLOANflag; it is only set between begin and end, so atrueread 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 }, andcompileFlashloanPrecheck(...)compiles the full message and reportsfullTxSize/overshootbefore you build. - Program 0.1.10. The
end_flashloanaccount layout changed in program 0.1.10;makeEndFlashLoanIx(and thereforemakeFlashLoanTx) builds the right layout for the live program, see Program Upgrade 0.1.10.
Related
- Looping and Collateral Swaps, Debt Swaps & Repay with Collateral — the SDK's built-in flash-loan flows
- Transactions — LUTs, oracle cranks, bundles, priority fees
- Architecture & API Layers — where
makeFlashLoanTxand themake*Ixmethods sit - Flashloans guide — protocol rules and use cases
- Examples: 16a-loop.ts shows a flash-loan flow end to end
Collateral Swaps, Debt Swaps & Repay with Collateral
Rebalance an open position atomically — swap one collateral for another, move debt between tokens, repay debt from collateral, or roll a matured Exponent PT — without touching account health mid-way.
Integration Banks & Native Stake
Deposit into and withdraw from Kamino, Drift and Jupiter Lend banks, and turn native stake accounts into LSTs you can deposit.