Back to Cheat Sheets

⚡ Flash Loan Attacks

OWASP Smart Contract Top 10 (2025) - SC07

HIGH RISK

📋 What Is It?

A flash loan is an uncollateralised loan that must be borrowed and repaid within a single transaction. Because the EVM executes a transaction atomically, if the loan plus its fee is not returned by the end of the same transaction, the entire transaction reverts-so the lender is never at risk. Flash loans are not themselves a vulnerability; they are a legitimate DeFi primitive. The security problem is that they are a devastating amplifier: a flash loan attack uses borrowed capital to weaponise some other weakness-a manipulable price feed, a naive vote count, an accounting bug-at a scale that would otherwise be impossible. The loan does not create a new door; it hands the attacker a battering ram for one transaction and hands it back before anyone can react.

SC07 OWASP Rank
Amplifier Not the Flaw
Risk-Free To Attempt

⚠️ Common Exploits

  • Spot-price oracle skew: flash swap distorts a single AMM pool, feeding a false price to the victim
  • Governance takeover: flash-borrow governance tokens, reach quorum, vote, and repay in one tx
  • Share / accounting drain: a rounding or share-price bug harmless at retail size becomes a full drain at scale
  • Liquidation gaming: move the reference price to force a healthy position underwater and liquidate it
  • Collateral / debt manipulation: spike a collateral price to mint under-collateralised debt
  • Reentrancy amplification: flash capital multiplies the value a reentrancy bug steals per callback
  • Arbitrage extraction: capture an entire cross-venue spread in one shot instead of a small slice

🔴 Attack Flow

1. Borrow a huge uncollateralised loan (one tx)
↓
2. Manipulate shared state the victim trusts
↓
3. Extract: borrow / mint / redeem / vote at bad state
↓
4. Unwind the manipulation, repay loan + fee
↓
5. IMPACT: Pool drained or governance captured - attacker risks only gas!

❌ Vulnerable Code

// Bad: price derived from a single pool's CURRENT reserves = spot price // A flash-funded swap can move this arbitrarily for one transaction function price() public view returns (uint) { (uint112 base, uint112 quote, ) = pair.getReserves(); return uint(quote) * 1e18 / uint(base); ← spot = trivially skewable } function borrow() external view returns (uint maxBorrow) { maxBorrow = collateral[msg.sender] * price() / 1e18; ← trusts the fake price } // Bad: voting weight = CURRENT balance - a flash borrower's weight counts fully function castVote(uint proposalId) external { forVotes[proposalId] += govToken.balanceOf(msg.sender); ← flash-inflatable } // Bad: no timelock - vote and execute in the SAME transaction function execute(uint proposalId, address target, bytes calldata data) external { require(forVotes[proposalId] >= quorum(), "no quorum"); target.call(data); ← drains treasury at borrowed weight } // Bad: share price from live token balance - a flash "donation" inflates it function sharePrice() public view returns (uint) { return asset.balanceOf(address(this)) * 1e18 / totalShares; ← gameable }

✅ Secure Code

// Good: time-weighted average price - a flash loan cannot sustain the skew // AND cross-check an independent feed, reject on divergence uint32 constant WINDOW = 1800; // 30-minute TWAP window uint constant MAX_DEVIATION_BPS = 200; // 2% function price() public view returns (uint) { uint twapPrice = twap.consult(base, WINDOW); ← Uniswap V3-style TWAP uint refPrice = feed.latestPrice(base); ← Chainlink-style feed uint diff = twapPrice > refPrice ? twapPrice - refPrice : refPrice - twapPrice; require(diff * 10000 / refPrice <= MAX_DEVIATION_BPS, "deviation too large"); return twapPrice; // manipulation-resistant } // Good: vote weight from a PAST-block snapshot -> flash balance = 0 votes function castVote(uint id) external { p.forVotes += govToken.getPastVotes(msg.sender, p.snapshotBlock); ← ERC20Votes } // Good: timelock forces execution into a LATER transaction function queue(uint id) external { p.eta = block.timestamp + TIMELOCK; } function execute(uint id, address target, bytes calldata data) external { require(p.eta != 0 && block.timestamp >= p.eta, "timelocked"); target.call(data); ← cannot run in the vote's tx } // Good: share price from internal accounting + dead shares defeat inflation uint constant MINIMUM_LIQUIDITY = 1e3; ← seed unredeemable shares at init function sharePrice() public view returns (uint) { return totalAssets == 0 ? 1e18 : totalAssets * 1e18 / totalShares; // tracked accounting - a raw token donation does NOT move it } // Good: reentrancy guard + CEI so flash capital cannot re-enter a drain function withdraw(uint amount) external nonReentrant { balances[msg.sender] -= amount; // EFFECT first (bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTION last require(ok, "transfer failed"); }

✓ Prevention Checklist

  • Design every mechanism assuming infinite one-tx capital
  • Never price off a single-block spot value - use TWAP
  • Cross-check an aggregated/independent feed, reject on divergence
  • Read vote weight from a past-block snapshot, not live balance
  • Separate voting from execution with a timelock
  • Add reentrancy guards and Checks-Effects-Interactions ordering
  • Seed dead shares / minimum liquidity to defeat first-depositor inflation
  • Round in the protocol's favour; reject dust-rounding griefing
  • Derive value from internal accounting, not token.balanceOf(this)
  • Bound price deviations and add sanity checks on large swings
  • Add circuit breakers and per-block outflow caps
  • Break single-tx composability for the most sensitive actions

🔍 Detection & Tools

Analysis Tools:

Slither Mythril Echidna Foundry Manticore

Secure Building Blocks:

Uniswap V3 TWAP Chainlink Price Feeds OpenZeppelin ERC20Votes ReentrancyGuard

How to Test:

  • Fork mainnet, take a real flash loan in a test, and attempt each mechanism at attacker scale
  • Use deal() to simulate flash capital, then assert the manipulation must revert
  • Fuzz and invariant-test solvency and share-price monotonicity under extreme inputs
  • Assert reserves stay intact: vault.totalAssets() == initialAssets
  • Monitor on-chain for abnormal price deviations and large single-block flows
  • Alert on governance actions with unusual vote sources

🌍 Real-World Incident Classes

  • Flash-loan-amplified oracle manipulation: a lending or synthetic-asset protocol priced collateral from the spot reserves of a single AMM pool; attackers flash-borrowed, swapped to skew that pool, then borrowed or minted against the mispriced asset before unwinding and repaying - leaving the protocol insolvent once the price snapped back. Mechanism: trusting a single-block, single-source spot price that flash capital can move at will.
  • Flash-loan governance takeover: a governance system tallied voting power from the current token balance at vote time; an attacker flash-borrowed the governance token, passed a proposal that transferred value or altered parameters in their favour, and repaid - all in one transaction. Mechanism: voting weight read from a live, flash-inflatable balance with no execution timelock.
  • Flash-loan-amplified accounting / reentrancy drain: a vault or pool held a share-pricing, reward, or reentrancy flaw harmless at small scale; flash-borrowed capital pushed the same flawed path to move a catastrophic amount in a single transaction. Mechanism: a logic/accounting or reentrancy weakness never stress-tested against effectively unlimited one-tx capital.

Note: these are described as classes of well-documented DeFi attacks. Specific protocol names and dollar figures vary by source and are deliberately omitted; the durable lesson is that the root vulnerability is almost never the loan - it is the victim's reliance on a manipulable price, balance, or formula.

📌 Quick Tips

  • DO NOT price off a single-pool spot value
  • DO NOT read vote weight from a live balance
  • DO NOT let a proposal vote and execute in one tx
  • DO use TWAP + an independent reference feed
  • DO snapshot votes from a past block + timelock
  • DO add reentrancy guards, deviation bounds, and per-block caps
  • DO test every mechanism at attacker-scale capital