# 30 — The case of the flash-loan reentrancy

> Scenario file: `scenarios/flash_loan_reentrancy.yaml` Severity observed: CRITICAL Time-to-reproduce: 20 minutes

This case is the longest in the book because it is the one we lose the most sleep over. When this fires, the protocol has lost at least one full liquidity event and usually much more. We have seen it three times in production audits and each time it cost more than we expected to find.

## What flash-loan reentrancy means on Solana

The phrase "reentrancy" arrived from EVM literature, where it means a contract calling back into another contract before the first call has returned. Solana's runtime does not have reentrancy in that sense. What we mean here is something subtly different and, in our experience, more dangerous: **a flash loan that lets the adversary use the protocol's own funds against an invariant the protocol is verifying&#x20;*****after*****&#x20;the loan has been taken**.

Concretely: the adversary borrows from a flash-loan provider, uses the borrowed asset to manipulate the *target* protocol's invariant, then triggers the target protocol's instruction that depends on that invariant. The borrowed asset is repaid in the same transaction. The target protocol never sees a balance discrepancy because the loan is balanced; it sees a momentarily true invariant that is in fact an artifact of the loan.

## The setup

A vault protocol mints share tokens at the ratio of `total_assets / total_shares`. Both numerator and denominator are read from on-chain state at the moment of mint. The protocol does not lock the underlying asset's price during the same transaction.

```rust
let shares_minted = deposit.checked_mul(total_shares)? / total_assets;
```

The bug is that `total_assets` is computed from a pool's reserves, and the reserves can be moved by a single swap inside the same transaction. The adversary's flash loan funds that swap.

## The attack

A six-step sequence:

1. flash-borrow $1,000,000 of asset A;
2. swap into the vault's underlying pool, pushing reserves so the vault's `total_assets` reads inflated;
3. deposit a tiny amount into the vault — say $1 — receiving an outsized number of shares because `total_assets` is artificially high;
4. swap back to asset A, restoring the pool;
5. burn the shares for the underlying, which is now worth far more than the $1 deposit;
6. repay the flash loan from the proceeds.

Net adversary profit equals the share dilution captured in step 5, minus pool fees in steps 2 and 4, minus the flash-loan fee.

## What the fuzzer does

`flash_loan_reentrancy.yaml` declares the six-step sequence and asks the fuzzer to find the borrow size that maximises profit. The genetic fuzzer is the right tool: too small a borrow and the inflation is not worth the fees; too large and the swap eats more in pool fees than the dilution returns.

In the runs we logged, the optimum borrow was almost always between 5x and 25x the pool's TVL. Below 5x the swap did not move reserves enough; above 25x the pool fees ate the inflation.

## The fix

There is no single fix; there are three valid strategies and they compose.

**TWAP the read.** Use a time-weighted average of the pool's reserves over a window the adversary cannot dominate within a single transaction. A 15-minute TWAP is enough to make the attack uneconomic on most chains. This is the cheapest fix to deploy and the one with the worst trade-off — it makes the protocol slower to respond to legitimate price moves.

**Independent valuation.** Compute `total_assets` from a source that is not a pool the adversary can swap against. A Pyth feed is the obvious candidate, but it has its own failure modes (chapter 10). For some protocols a constant — "$1 per stablecoin" — is sufficient.

**Single-instruction commit.** Require the deposit and the share-mint to land in separate instructions, and read `total_assets` only once at the start of the deposit. The adversary can still manipulate reserves between the two instructions, but the manipulation no longer affects share-mint quantities.

The right combination depends on the protocol. Vaults that can tolerate slowness should pick the TWAP. Vaults whose underlying is illiquid should pick independent valuation. Vaults with predictable underlying value should pick the single-instruction commit.

The hardening index entry is `FLASHLOAN-REENT-1`. The hardening index in chapter 80 is also where we list the seven specific protocols whose audit reports we have studied that include a fix in this family — useful if you would rather read someone else's resolved finding than design from first principles.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://economicfuzz.gitbook.io/economicfuzz-docs/case-studies/30-case-flashloan-reentrancy.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
