All articles
Smart Contracts·March 8, 2026·6 min read

Flash-Loan Arbitrage in a Single Solana Program Atomic Bundle

Solana does not have native flash loans, but you can replicate the pattern using same-transaction CPI sequences across Marginfi and Kamino. This guide shows how to structure the borrow, swap, and repay CPIs inside one Anchor instruction to guarantee atomicity.

Solana does not have a native flash loan primitive. There is no single protocol that issues uncollateralized funds and reverts the entire transaction if repayment fails — at least not in the way Aave does on Ethereum. What it does have is something arguably more powerful: composable cross-program invocations (CPIs) within a single atomic transaction, where any instruction failure rolls back every state change across every program touched. You can build flash-loan semantics on top of that guarantee, and this post shows exactly how.

The Core Insight: Atomicity Is the Flash Loan

On Ethereum, a flash loan is a smart contract primitive — the lender enforces repayment by reverting unless the ending balance matches the starting balance. On Solana, the enforcement mechanism is the transaction itself. If any CPI in your instruction sequence returns an error, the runtime rolls back all account mutations across all programs in that transaction. No partial state persists.

This means you can structure a borrow-swap-repay sequence inside a single Anchor instruction handler, and the protocol lending you funds does not need flash-loan-specific logic. If your swap produces enough to repay plus fees, the transaction succeeds. If the arb leg fails or the repayment leg would overdraw, the entire transaction reverts. The lender's funds were never at risk because the transaction either commits all steps atomically or commits none.

The protocols that make this practical today are Marginfi and Kamino, both of which permit CPI borrows from within the same transaction as the repayment, as long as the accounts are structured correctly and the bank's utilization constraints are respected.

Instruction Layout Inside an Anchor Handler

The typical handler signature looks like this:

pub fn flash_arb(ctx: Context<FlashArb>, borrow_amount: u64, min_profit: u64) -> Result<()> {
    // 1. Borrow from Marginfi bank via CPI
    // 2. Execute swap on Jupiter aggregator via CPI
    // 3. Repay Marginfi (principal + interest) via CPI
    // 4. Assert profit exceeds min_profit or return Err
    Ok(())
}

Each numbered step is a CPI call. The accounts context must include all accounts required by Marginfi, Jupiter, and your own program simultaneously — this is where the transaction size ceiling becomes the binding constraint. Solana's 1,232-byte transaction size limit and the 64-account limit per transaction are the two walls you will hit in production. Marginfi's lending_account_borrow instruction alone requires around 14 accounts; Jupiter's route can add another 15–30 depending on hop count. You frequently have to pre-compute the cheapest Jupiter route that fits within your account budget, not simply the most profitable route.

The account ordering matters. Marginfi validates that the same lending account is present in both the borrow and repay instructions. Pass accounts in the wrong order and you get a constraint violation before any funds move.

Marginfi vs Kamino: Choosing Your Lender

Both protocols support CPI borrows, but the mechanics differ in ways that affect your strategy selection.

Marginfi uses a bank model with per-asset utilization caps. Each bank has a borrow_limit and a utilization_limit; if your borrow would push utilization past the ceiling, the CPI fails. At peak hours, USDC and SOL banks regularly sit above 85% utilization. You need a pre-flight utilization check before building and sending the bundle — there is no point paying priority fees on a transaction that will fail at the lending step.

Kamino structures liquidity across reserves within obligation accounts. CPI borrows require an existing obligation owned by your program's PDA, which you initialize once and reuse. The reserve's available liquidity is simpler to query — read the liquidity_available_amount field off the reserve account — but Kamino applies a flash-loan fee on top of the standard borrow APR. As of recent reserve parameters, this fee has been in the 5–9 bps range depending on the asset. That cost must be baked into your minimum profit threshold before you construct the route.

In practice, the choice usually comes down to which protocol has headroom for the asset and size you need at the moment you need it. We maintain live reserve snapshots for both and select the lender at bundle-construction time.

Simulation Before You Submit

Never submit a flash arb bundle without a simulateTransaction call first. The simulation must use replaceRecentBlockhash: true and commitment: "processed" to reflect the freshest account state. What you are checking:

  • Borrow succeeds: utilization is under the cap at the simulated block
  • Swap output meets repayment + fee + min_profit: confirm the Jupiter quote you used to construct the route still holds; prices move between route computation and simulation
  • No compute budget overflow: flash arb instructions are CU-heavy; set your ComputeBudgetInstruction::set_compute_unit_limit conservatively high during development, then tune it down to the simulation's actual consumption plus a 15% buffer

A failed simulation costs you nothing. A failed on-chain transaction costs priority fees and the Jito tip. On a congested network those tips run 0.01–0.05 SOL on competitive arb routes. Simulate every time.

Priority Fees, Jito Tips, and When Bundles Are Worth It

For flash arb, a Jito bundle is often not necessary — the transaction is self-contained and there is no sandwich risk because you are the only party moving funds. A standard priority-fee transaction routed to multiple validators usually fills within 1–2 blocks on a non-congested cluster.

Where bundles earn their cost is when you need to pair the arb with a position close on another protocol — for example, unwinding a perp on Drift in the same block that the flash arb repays the lending position. In that case, atomicity across programs within a single transaction handles it, but if the close is a separate transaction, you want a Jito bundle to guarantee ordering. The bundle tip on a two-transaction bundle for a $50k arb is typically 0.02–0.05 SOL; at 5 bps profit on notional that is still deeply in the money.

Compute unit pricing on competitive arb routes runs 200,000–500,000 microlamports per compute unit during busy periods. Build a dynamic fee oracle that reads the last 20-slot median from getRecentPrioritizationFees filtered to your program ID, and apply a 1.3–1.5x multiplier. Flat fees leave money on the table in slow periods and get you dropped in fast ones.

What Can Go Wrong in Production

The failure modes that will actually cost you money:

  • Stale Jupiter route: the AMM pool state shifts between route computation and transaction submission; the swap output falls short of the repayment amount and the entire transaction reverts. Solution: build the route fresh within 300ms of submission, not from a cache.
  • Utilization race: a large borrow hits the Marginfi bank 200ms before yours; your borrow CPI fails at utilization check. Solution: pre-flight check plus fallback to Kamino if Marginfi is saturated.
  • Account size mismatch: Anchor's #[account] constraint macros deserialize account data at instruction entry; if you pass a Kamino reserve account where Marginfi expects a bank, you get a discriminator mismatch error before any funds move. Test every account layout combination with a local validator fork before touching mainnet.
  • Missing signer seeds: your program's PDA must sign the repayment CPI. Forgetting to pass invoke_signed with the correct seeds is the most common first-deploy failure.

This is the kind of system we build and ship for clients on our trading-bot services side — from Anchor program design through simulation infrastructure and live monitoring. If you are working on a flash arb implementation and want production-grade architecture instead of a weekend hack, talk to us.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Smart Contracts#Arbitrage#Anchor#DeFi#MEV