$3.5MMarch 14, 20268 min read

How a Flash Loan Turned $10M Into a $3.5M Theft: The Nirvana Finance Exploit

SolanaDeFiFlash LoansExploit AnalysisPrice ManipulationBorrow/Lend

July 2022 was a brutal month for Solana DeFi. Earlier in the month, Crema Finance - a Solana-based concentrated liquidity protocol - lost $6 million to a flash loan attack. The ecosystem was still reeling when, on July 28, Nirvana Finance became the next victim. The two attacks shared a common thread: flash loans sourced from Solend, a custom attacker program uploaded to the chain, and a protocol that did not account for price manipulation within a single transaction. Nirvana lost $3.49 million. Its ANA token collapsed 85% in hours. The NIRV stablecoin lost its peg, falling to $0.12. The treasury was emptied. It was a rough week for Solana DeFi - and a masterclass in why flash-loan attack surfaces demand first-class treatment in protocol design.

What is Nirvana Finance?

Nirvana Finance was an adaptive yield protocol on Solana. It issued two tokens: ANA, a supercollateralized store-of-value token with an algorithmically rising floor price, and NIRV, a dollar-pegged stablecoin backed by ANA. The protocol was designed around a bonding curve - users could mint ANA by depositing USDC or USDT into the treasury, and the protocol would quote a price based on available liquidity. The floor price of ANA was supposed to only ever move upward, backed by real treasury assets. Users could also borrow NIRV against their ANA holdings at a ratio tied to the current ANA price. This design had a critical hidden assumption: that ANA's price could not be manipulated within the span of a single transaction.

Flash Loans on Solana - How They Work

Flash loans on Ethereum typically use a callback pattern with a lending pool - borrow funds, execute logic in a callback, repay in the same block. On Solana, flash loans work differently. There are no callbacks in the Ethereum sense. Instead, Solana's transaction model allows a program to use CPI (cross-program invocations) to call into a lending protocol, receive funds, execute arbitrary instructions, and repay - all within a single atomic transaction. If the repayment fails, the entire transaction reverts. Solend, Solana's largest lending protocol at the time, offered flash loans from its liquidity pools. An attacker can write a custom Solana program, deploy it, execute the exploit transaction (which calls into Solend, does its damage, and repays), then close the program to remove on-chain evidence. That is exactly what happened here. The attacker uploaded a purpose-built exploit program to the Solana blockchain, executed one transaction, and deleted the program. The whole operation was atomic - and permanent.

The Attack Step by Step

The exploit unfolded in a single Solana transaction with devastating precision:

1. Borrow $10M USDC via Solend flash loan - The attacker's program called into Solend and borrowed $10 million USDC from the main pool.

2. Mint $10M worth of ANA tokens - The USDC was deposited into the Nirvana treasury, minting ANA tokens at the current bonding curve price (roughly $8 per ANA). This injection of $10M into the treasury caused the quoted ANA price to spike dramatically.

3. ANA price inflates from $8 to $24 - The protocol's bonding curve responded to the large liquidity deposit by raising the quoted price. With $10M in fresh USDC sitting in the treasury, the protocol believed ANA was now worth roughly $24.

4. Borrow NIRV against inflated ANA - The attacker used their ANA holdings as collateral to borrow NIRV stablecoin at the inflated $24 valuation, extracting far more value than the ANA was actually worth.

5. Redeem ANA for USDT at the inflated price - The attacker swapped their $10M worth of ANA tokens back for $13.49 million USDT, using the treasury's own reserves at the artificial price.

6. Repay the $10M USDC flash loan - With $13.49M USDT in hand, the attacker repaid Solend's $10M USDC loan.

7. Exit with $3.49M profit - The attacker pocketed approximately $3.49 million, bridged the stolen funds to Ethereum, and converted them to DAI. The program was closed immediately after the transaction.

Why the Protocol Was Vulnerable

The root cause was a single dangerous assumption: that a price derived from treasury reserves at the start of a function could be trusted to remain valid throughout that same transaction. Nirvana's borrow logic read the ANA price from the bonding curve state and used it to calculate how much NIRV a user could borrow. But there was nothing preventing that same transaction from having first manipulated the bonding curve by injecting a massive amount of USDC.

Three specific failures compounded the damage:

- No intra-transaction price isolation: The protocol's mint and borrow operations shared the same price oracle (the bonding curve state), with no mechanism to distinguish between a legitimately appreciated price and one that had been inflated by a deposit in the same transaction.

- No borrow caps or rate limits: There was no ceiling on how much could be borrowed in a single transaction or over a short time window. The attacker could extract value limited only by the treasury size.

- No collateral lock period: ANA minted and used as collateral in the same transaction should have been impossible - or at minimum flagged as high-risk. A slot-delay or pre/post balance assertion would have blocked this path.

Solend's flash loan infrastructure was not at fault - it worked exactly as designed. The vulnerability was entirely in how Nirvana's program handled prices during a transaction that included its own large deposits.

rust
// VULNERABLE PATTERN - do not use
// The bonding curve price is read from current reserve state.
// An attacker can deposit $10M in the same transaction to inflate this.

pub fn borrow_nirv(ctx: Context<BorrowNirv>, amount: u64) -> Result<()> {
    let treasury = &ctx.accounts.treasury;

    // DANGEROUS: price derived from live reserve ratio
    // If an attacker deposited into the treasury earlier in this
    // same transaction, this price is now inflated.
    let ana_price = treasury.total_usdc_reserves
        .checked_div(treasury.ana_supply)
        .ok_or(ErrorCode::MathOverflow)?;

    let collateral_value = ctx.accounts.user_ana_balance
        .checked_mul(ana_price)
        .ok_or(ErrorCode::MathOverflow)?;

    // No borrow cap, no slot check, no deposit-in-same-tx guard.
    // Attacker can borrow against 3x inflated collateral.
    let max_borrow = collateral_value
        .checked_mul(LTV_RATIO)
        .ok_or(ErrorCode::MathOverflow)?;

    token::mint_to(
        ctx.accounts.into_mint_nirv_context(),
        max_borrow,
    )?;

    Ok(())
}

Building Flash-Loan-Resistant DeFi on Solana

Defending against this class of attack requires treating any price or collateral value computed during a transaction as potentially tainted. There are several proven patterns for Solana programs:

- Slot-based price staleness checks: Record the slot at which a deposit or price update occurred. Reject borrow operations that use a price updated in the same slot as a large deposit. This breaks the atomic manipulation chain.

- Pre/post balance assertions: Use Solana's instruction introspection to record treasury balances at the start of the transaction. If a borrow instruction detects the treasury grew by more than a threshold in the same transaction, reject it.

- Borrow caps per transaction: Cap the maximum collateral value that can be used to back a borrow in any single transaction. Even if the price is manipulated, the attacker cannot extract more than the cap allows.

- Twap price feeds: Use time-weighted average prices from an oracle (Pyth, Switchboard) instead of computing price from instantaneous reserve ratios. A TWAP cannot be manipulated within a single slot.

The fix is not complex. It requires treating flash-loan-amplified deposits as a first-class threat model, not an afterthought.

rust
// FLASH-LOAN-RESISTANT PATTERN
// Uses slot-based staleness, per-tx borrow cap, and TWAP price feed.

pub fn borrow_nirv(ctx: Context<BorrowNirv>, amount: u64) -> Result<()> {
    let treasury = &ctx.accounts.treasury;
    let clock = Clock::get()?;

    // Guard 1: Reject if treasury received a large deposit this slot.
    // Any deposit updates treasury.last_deposit_slot.
    require!(
        clock.slot > treasury.last_deposit_slot + SLOT_DELAY,
        ErrorCode::PriceStale
    );

    // Guard 2: Use a TWAP price from an external oracle (Pyth/Switchboard).
    // This cannot be manipulated within a single transaction.
    let ana_price = get_twap_price(&ctx.accounts.price_feed)?;

    // Guard 3: Enforce a per-transaction borrow cap.
    // Even with a valid price, no single tx can extract more than MAX_BORROW.
    let requested = amount;
    require!(
        requested <= MAX_BORROW_PER_TX,
        ErrorCode::BorrowCapExceeded
    );

    let collateral_value = ctx.accounts.user_ana_balance
        .checked_mul(ana_price)
        .ok_or(ErrorCode::MathOverflow)?;

    let max_allowed = collateral_value
        .checked_mul(LTV_RATIO)
        .ok_or(ErrorCode::MathOverflow)?;

    require!(requested <= max_allowed, ErrorCode::InsufficientCollateral);

    // Guard 4: Pre/post balance assertion - record expected post-borrow state
    // and verify in a separate instruction to detect mid-tx manipulation.
    treasury.expected_post_borrow_reserves = treasury
        .total_usdc_reserves
        .checked_sub(amount)
        .ok_or(ErrorCode::MathOverflow)?;

    token::mint_to(
        ctx.accounts.into_mint_nirv_context(),
        amount,
    )?;

    Ok(())
}

fn get_twap_price(price_feed: &AccountInfo) -> Result<u64> {
    // Read from Pyth or Switchboard - returns a time-weighted average
    // computed over many slots, immune to single-transaction manipulation.
    let price = load_price_feed_from_account_info(price_feed)?
        .get_ema_price_unchecked();
    Ok(price.price as u64)
}

RedPen Takeaway

When RedPen audits a borrow/lend or stablecoin protocol on Solana, flash loan attack surfaces are a mandatory checklist item - not an optional concern. We look for three specific failure patterns: (1) price reads that derive value from the same state that can be modified earlier in the same transaction - especially bonding curves, reserve ratios, or AMM spot prices; (2) the absence of borrow caps per transaction or per slot, which turn a price manipulation bug into a full treasury drain; and (3) the absence of slot-delay guards between deposit and collateralization operations. We also test whether TWAP or time-weighted price feeds are used anywhere collateral value is computed, and we attempt to construct single-transaction exploit paths that chain deposit, price read, and withdrawal instructions. If a protocol's borrow math trusts any value that the borrower can influence in the same transaction, that is a critical finding. The Nirvana exploit was entirely preventable with a two-line slot check.

Your DeFi protocol deserves an audit before it faces a live attacker - not after. Let RedPen find your flash loan attack surface first.

RedPen audits Anchor programs for exactly this class of vulnerability. Get your program reviewed before mainnet.