IAMUVIN

Blockchain Security

Flash Loan Attacks: How They Work and How to Defend Against Them

Uvin Vindula·June 3, 2024·12 min read
Share

TL;DR

Flash loans are uncollateralized loans that must be borrowed and repaid within a single transaction. They are one of DeFi's most powerful primitives — and one of its most dangerous attack vectors. Since 2020, flash loan attacks have drained over $1 billion from protocols through oracle manipulation, governance hijacking, and price exploitation. I check for flash loan attack surfaces in every blockchain security audit I perform, and the patterns keep repeating. This article breaks down exactly how flash loan attacks work, walks through real exploits (Euler Finance lost $197 million, Beanstalk lost $182 million), and gives you concrete Solidity defense patterns — TWAP oracles, timelocks, circuit breakers, and invariant checks — that you can deploy today.


What Flash Loans Are

To understand flash loan attacks, you first need to understand why flash loans exist and what makes them fundamentally different from every other type of lending in human history.

In traditional finance, borrowing requires collateral. You put up your house to get a mortgage. You lock up assets to get a margin loan. The collateral exists to protect the lender — if you default, they seize your collateral.

Flash loans eliminate collateral entirely by exploiting a property unique to blockchains: atomic transactions. On Ethereum, a transaction either executes completely or it reverts entirely. There is no partial execution. This means a lending protocol can give you $200 million with zero collateral, as long as you return it within the same transaction. If you fail to repay, the entire transaction reverts — including the loan itself. The lender never actually loses funds because the loan never actually happened.

Here is the simplified flow:

  1. Borrower requests a flash loan of 1,000,000 USDC from Aave
  2. Aave transfers 1,000,000 USDC to the borrower's contract
  3. Borrower's contract executes arbitrary logic (arbitrage, liquidations, collateral swaps)
  4. Borrower repays 1,000,000 USDC plus a 0.09% fee back to Aave
  5. Aave verifies the repayment — if it is short, the entire transaction reverts

The legitimate use cases are real. Arbitrage traders use flash loans to equalize prices across DEXs without needing capital. Users refinance collateral between lending protocols in a single transaction. Liquidators clear unhealthy positions without holding inventory.

But the same atomic composability that enables these use cases also enables attacks. An attacker can borrow hundreds of millions of dollars, manipulate a protocol's price feed or governance system, extract value, repay the loan, and walk away with the profit — all in one transaction. The cost of the attack is just the gas fee and the flash loan fee. For a $200 million loan on Aave, that is roughly $180,000 in fees plus maybe $50 in gas. Compare that to the tens or hundreds of millions in profit.

This asymmetry — massive capital for minimal cost — is what makes flash loan attacks so devastating.


How Flash Loan Attacks Work — Step by Step

Every flash loan attack I have analyzed follows the same fundamental pattern. The specifics vary — different protocols, different price feeds, different extraction mechanisms — but the skeleton is always this:

Step 1: Borrow massive capital via flash loan. The attacker deploys a smart contract that borrows millions from Aave, dYdX, or another flash loan provider. Some attacks chain multiple flash loans from different providers to maximize capital.

Step 2: Use the capital to create an artificial market condition. This is where the attack diverges based on the vector. The attacker might:

  • Dump a huge amount of Token A into a DEX pool, crashing its spot price
  • Buy a massive amount of Token B on a thin liquidity pool, pumping its price
  • Deposit into a lending protocol to inflate share prices
  • Acquire governance tokens to pass a malicious proposal

Step 3: Exploit a protocol that relies on the manipulated condition. A lending protocol that uses the manipulated spot price as its oracle now thinks the collateral is worth more (or the debt is worth less). The attacker borrows against inflated collateral, or liquidates positions that are not actually unhealthy.

Step 4: Reverse the manipulation. The attacker unwinds their position — sells back the tokens, withdraws from the pool — returning the market to roughly its original state.

Step 5: Repay the flash loan and keep the profit. The difference between what the attacker extracted from the vulnerable protocol and what they paid for the flash loan plus fees is pure profit.

The entire sequence executes in a single transaction. If any step fails — if the profit is not enough to repay the loan — the whole thing reverts. The attacker loses nothing except the gas fee for the failed transaction. This is risk-free exploitation.

Here is a simplified attack contract:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IFlashLoanReceiver} from "@aave/v3-core/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPool} from "@aave/v3-core/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IVulnerableDEX {
    function swap(address tokenIn, address tokenOut, uint256 amountIn) external returns (uint256);
    function getSpotPrice(address token) external view returns (uint256);
}

interface IVulnerableLending {
    function deposit(address token, uint256 amount) external;
    function borrow(address token, uint256 amount) external;
    function withdraw(address token, uint256 amount) external;
}

/// @notice Simplified flash loan attack — for educational purposes only
contract FlashLoanAttackExample {
    IPool public immutable aavePool;
    IVulnerableDEX public immutable vulnerableDex;
    IVulnerableLending public immutable vulnerableLending;
    address public immutable attacker;

    constructor(address _pool, address _dex, address _lending) {
        aavePool = IPool(_pool);
        vulnerableDex = IVulnerableDEX(_dex);
        vulnerableLending = IVulnerableLending(_lending);
        attacker = msg.sender;
    }

    function executeAttack(address token, uint256 amount) external {
        require(msg.sender == attacker, "Not attacker");
        // Step 1: Borrow via flash loan
        aavePool.flashLoanSimple(address(this), token, amount, "", 0);
    }

    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata
    ) external returns (bool) {
        require(msg.sender == address(aavePool), "Not Aave");
        require(initiator == address(this), "Not self");

        // Step 2: Dump tokens into vulnerable DEX to crash price
        IERC20(asset).approve(address(vulnerableDex), amount);
        vulnerableDex.swap(asset, address(0), amount / 2);

        // Step 3: Exploit the crashed price on the lending protocol
        // The lending protocol reads the DEX spot price as its oracle
        // Price is now artificially low — borrow cheap
        vulnerableLending.borrow(asset, amount / 4);

        // Step 4: Reverse the manipulation
        vulnerableDex.swap(address(0), asset, amount / 2);

        // Step 5: Repay flash loan + fee
        uint256 totalDebt = amount + premium;
        IERC20(asset).approve(address(aavePool), totalDebt);

        return true;
    }
}

This is intentionally simplified. Real attacks are more complex — multiple pools, multiple tokens, precise calculations of slippage and extraction amounts. But the pattern is identical.


Real Attack Case Studies

Euler Finance — March 13, 2023 — $197 Million

The Euler Finance exploit remains one of the largest flash loan attacks in DeFi history. The attacker used a sequence of flash loans to exploit a vulnerability in Euler's donation mechanism.

The timeline:

  • 13:xx UTC: The attacker deployed a set of contracts on Ethereum mainnet
  • 13:xx UTC: Executed a series of transactions borrowing DAI, WBTC, USDC, and stETH through flash loans
  • Within minutes: Drained $197 million across multiple token markets

The mechanism:

Euler allowed users to donate their eTokens (deposit tokens) to the Euler reserves. The attacker:

  1. Flash-borrowed a massive amount of DAI
  2. Deposited into Euler, receiving eTokens
  3. Used the eTokens as collateral to borrow more DAI via Euler's leverage function
  4. Donated a portion of the eTokens to Euler reserves — this was the critical step
  5. The donation made their position appear insolvent, but Euler's liquidation math did not properly account for the donated amount
  6. Self-liquidated at favorable terms, extracting more value than they deposited
  7. Repeated across multiple markets

The vulnerability was not the flash loan itself. The flash loan simply provided the capital to exploit a logic error in Euler's donation and liquidation accounting. Without the flash loan, the attacker would have needed $197 million in capital sitting around — which is why flash loans turn theoretical vulnerabilities into practical exploits.

In a rare outcome, the attacker eventually returned the funds after on-chain negotiations. But the protocol was effectively shut down for weeks, and the reputational damage was severe.

Beanstalk Farms — April 17, 2022 — $182 Million

The Beanstalk attack was different. Instead of price manipulation, the attacker used flash loans to hijack governance.

The timeline:

  • April 16: The attacker submitted Beanstalk Improvement Proposal 18 (BIP-18), a malicious governance proposal
  • April 17, 12:24 UTC: One day later (the minimum governance delay), the attacker executed the attack
  • Single transaction: Borrowed $1 billion via flash loans, voted on BIP-18, drained the protocol, repaid the loans

The mechanism:

  1. Flash-borrowed approximately $1 billion in stablecoins from Aave, Uniswap, and SushiSwap
  2. Deposited into Beanstalk's Silo (staking mechanism) to receive governance tokens
  3. Used the governance tokens to vote on and pass BIP-18 — which had an emergencyCommit function that allowed immediate execution once 67% quorum was reached
  4. BIP-18 transferred all protocol funds ($182 million) to the attacker
  5. Withdrew from the Silo, repaid all flash loans
  6. Net profit: approximately $80 million (after accounting for the flash loan fees and gas)

The core vulnerability: Beanstalk's governance allowed voting power to be acquired and exercised within the same transaction. There was no time-lock between acquiring governance tokens and using them to vote. A 24-hour proposal delay existed, but the attacker simply submitted the proposal a day in advance and waited.

This attack fundamentally changed how DeFi protocols think about governance. If governance power can be borrowed, governance can be bought for the price of a flash loan fee.


Oracle Manipulation via Flash Loans

The most common flash loan attack vector I encounter in audits is oracle manipulation. If a protocol relies on spot prices from a DEX as its price oracle, it is vulnerable to flash loan attacks. Full stop.

Here is why. A spot price on an AMM like Uniswap is simply the ratio of token reserves in the pool:

spotPrice = reserveA / reserveB

If an attacker dumps 10,000 ETH into a pool that holds 1,000 ETH and 2,000,000 USDC, the spot price changes dramatically in that instant. Any protocol that reads this price during the same transaction will get a manipulated value.

This is not a theoretical concern. I have seen production contracts that do this:

solidity
// VULNERABLE — reads manipulable spot price
function getCollateralValue(address token, uint256 amount) public view returns (uint256) {
    uint256 price = uniswapPool.getSpotPrice(token);
    return amount * price / 1e18;
}

The attacker borrows a flash loan, manipulates the pool price, and the lending protocol now values their collateral at 10x its real worth. They borrow against it, reverse the manipulation, repay the flash loan, and walk away with the difference.


Price Manipulation Vectors

Beyond simple oracle manipulation, flash loans enable several sophisticated price manipulation strategies that I check for in every audit:

Sandwich Attacks on Protocol Operations

When a protocol performs a large swap as part of its normal operation (rebalancing, yield harvesting, liquidations), an attacker can sandwich the transaction:

  1. Flash-borrow capital
  2. Front-run the protocol's swap by buying the target token, pushing the price up
  3. Protocol's swap executes at a worse price
  4. Back-run by selling the token at the inflated price
  5. Repay flash loan, keep the spread

Reserve Ratio Manipulation

Many DeFi protocols calculate share prices based on the ratio of assets to shares:

sharePrice = totalAssets / totalShares

An attacker can manipulate totalAssets by donating tokens directly to the vault contract before minting shares, or by manipulating the price of underlying assets. This is the basis of the ERC-4626 inflation attack and was a factor in several vault exploits throughout 2023.

Cross-Protocol Arbitrage Exploitation

Some protocols depend on consistent pricing across multiple venues. An attacker can:

  1. Manipulate the price on Protocol A (where the vulnerable protocol reads its oracle)
  2. Exploit the price discrepancy on Protocol B (which trusts Protocol A's price)
  3. Reverse the manipulation on Protocol A

This is particularly dangerous because Protocol B might have perfect code — the vulnerability is in its dependency on an external, manipulable price source.

Liquidity Manipulation

Thin liquidity pools are especially vulnerable. If a protocol uses a pool with $500,000 in liquidity as its price oracle, an attacker can move the price with a relatively small flash loan. I always check the liquidity depth of any pool a protocol depends on. If the TVL is under $10 million, I flag it as a flash loan risk.


Governance Attacks

The Beanstalk exploit opened a new category of flash loan attacks: governance hijacking. Any protocol where governance power can be acquired and exercised within a single transaction is vulnerable. Here is what I look for:

Instant voting power: Can a user acquire tokens and vote in the same block? If yes, flash-borrowed tokens can control governance.

Low quorum thresholds: If passing a proposal requires 10% of total supply and flash loan providers hold more than 10%, governance can be bought.

Insufficient timelocks: A 24-hour timelock means nothing if the attacker can submit a proposal in advance and wait. Meaningful timelocks need to exceed the flash loan economic incentive — typically 48-72 hours minimum, with active monitoring.

Emergency functions: Functions that bypass timelocks (like Beanstalk's emergencyCommit) are the highest-risk governance patterns. If an emergency function can transfer funds and can be triggered by governance, it is a flash loan target.

The defense is straightforward in principle but requires discipline:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

/// @notice Governance token with flash loan resistance
/// @dev Voting power uses checkpoint from PREVIOUS block
contract FlashLoanResistantGovernance {
    ERC20Votes public immutable governanceToken;
    uint256 public constant PROPOSAL_DELAY = 2 days;
    uint256 public constant VOTING_PERIOD = 3 days;
    uint256 public constant TIMELOCK_DELAY = 48 hours;

    struct Proposal {
        uint256 id;
        address proposer;
        uint256 snapshotBlock;
        uint256 startTime;
        uint256 endTime;
        uint256 forVotes;
        uint256 againstVotes;
        bool executed;
        bytes32 actionHash;
    }

    mapping(uint256 => Proposal) public proposals;
    uint256 public proposalCount;

    function propose(bytes32 actionHash) external returns (uint256) {
        // Proposer must have held tokens for at least 1 block
        require(
            governanceToken.getPastVotes(msg.sender, block.number - 1) > 0,
            "Must hold tokens in previous block"
        );

        uint256 proposalId = ++proposalCount;
        proposals[proposalId] = Proposal({
            id: proposalId,
            proposer: msg.sender,
            snapshotBlock: block.number, // Snapshot at proposal creation
            startTime: block.timestamp + PROPOSAL_DELAY,
            endTime: block.timestamp + PROPOSAL_DELAY + VOTING_PERIOD,
            forVotes: 0,
            againstVotes: 0,
            executed: false,
            actionHash: actionHash
        });

        return proposalId;
    }

    function vote(uint256 proposalId, bool support) external {
        Proposal storage p = proposals[proposalId];
        require(block.timestamp >= p.startTime, "Voting not started");
        require(block.timestamp <= p.endTime, "Voting ended");

        // Use voting power from the SNAPSHOT BLOCK, not current block
        // Flash loans cannot retroactively change past block state
        uint256 weight = governanceToken.getPastVotes(
            msg.sender,
            p.snapshotBlock
        );
        require(weight > 0, "No voting power at snapshot");

        if (support) {
            p.forVotes += weight;
        } else {
            p.againstVotes += weight;
        }
    }
}

The key defense: voting power is determined by a past block snapshot, not the current block. Flash loans exist within a single transaction in a single block. They cannot change the state of previous blocks. By using getPastVotes with a snapshot from the proposal creation block, flash-borrowed tokens have zero governance influence.


Defense Strategies — TWAP Oracles, Timelocks, and Circuit Breakers

After auditing dozens of DeFi protocols, I have narrowed down the flash loan defenses that actually work in production. Theory is easy — implementation is where protocols fail.

TWAP Oracles (Time-Weighted Average Price)

The most effective defense against oracle manipulation is refusing to use spot prices entirely. A TWAP oracle averages the price over a time window, making single-block manipulation economically infeasible.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice TWAP oracle resistant to flash loan manipulation
/// @dev Maintains a ring buffer of price observations
contract TWAPOracle {
    struct Observation {
        uint256 timestamp;
        uint256 priceCumulative;
    }

    Observation[] public observations;
    uint256 public constant TWAP_PERIOD = 30 minutes;
    uint256 public constant MIN_OBSERVATIONS = 6;
    uint256 public constant MAX_DEVIATION_BPS = 500; // 5%

    address public immutable trustedUpdater;

    constructor(address _updater) {
        trustedUpdater = _updater;
    }

    /// @notice Record a new price observation
    /// @dev Called periodically by a keeper or on each user interaction
    function recordObservation(uint256 currentPrice) external {
        require(msg.sender == trustedUpdater, "Unauthorized");
        require(currentPrice > 0, "Invalid price");

        uint256 lastCumulative = observations.length > 0
            ? observations[observations.length - 1].priceCumulative
            : 0;

        uint256 lastTimestamp = observations.length > 0
            ? observations[observations.length - 1].timestamp
            : block.timestamp;

        uint256 elapsed = block.timestamp - lastTimestamp;

        observations.push(Observation({
            timestamp: block.timestamp,
            priceCumulative: lastCumulative + (currentPrice * elapsed)
        }));
    }

    /// @notice Get the TWAP price over the configured period
    /// @dev Reverts if insufficient observation history exists
    function getTWAP() external view returns (uint256) {
        require(observations.length >= MIN_OBSERVATIONS, "Insufficient data");

        Observation memory latest = observations[observations.length - 1];

        // Find the observation closest to TWAP_PERIOD ago
        uint256 targetTimestamp = latest.timestamp - TWAP_PERIOD;
        uint256 oldIndex = _findObservation(targetTimestamp);
        Observation memory old = observations[oldIndex];

        uint256 elapsed = latest.timestamp - old.timestamp;
        require(elapsed > 0, "Zero elapsed time");

        uint256 twapPrice = (latest.priceCumulative - old.priceCumulative) / elapsed;

        return twapPrice;
    }

    /// @notice Validate a price against TWAP to detect manipulation
    function validatePrice(uint256 spotPrice) external view returns (bool) {
        uint256 twap = this.getTWAP();
        uint256 deviation = spotPrice > twap
            ? ((spotPrice - twap) * 10_000) / twap
            : ((twap - spotPrice) * 10_000) / twap;

        return deviation <= MAX_DEVIATION_BPS;
    }

    function _findObservation(uint256 targetTimestamp) internal view returns (uint256) {
        for (uint256 i = observations.length - 1; i > 0; i--) {
            if (observations[i].timestamp <= targetTimestamp) {
                return i;
            }
        }
        return 0;
    }
}

A 30-minute TWAP means an attacker would need to maintain price manipulation across multiple blocks for at least 30 minutes. The cost of doing so — holding massive positions against arbitrageurs — makes the attack economically unfeasible. Uniswap V3's built-in oracle uses this exact approach with geometric TWAPs.

Circuit Breakers

Circuit breakers halt protocol operations when anomalous conditions are detected. They are your last line of defense when other protections fail.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Circuit breaker that pauses operations during anomalous activity
abstract contract CircuitBreaker {
    uint256 public constant VOLUME_WINDOW = 1 hours;
    uint256 public constant MAX_VOLUME_USD = 10_000_000e18; // $10M per hour
    uint256 public constant PRICE_CHANGE_THRESHOLD_BPS = 1000; // 10%
    uint256 public constant COOLDOWN_PERIOD = 30 minutes;

    uint256 public windowStart;
    uint256 public windowVolume;
    uint256 public lastKnownPrice;
    uint256 public circuitBrokenUntil;

    event CircuitBroken(string reason, uint256 timestamp, uint256 cooldownEnd);

    modifier circuitBreakerCheck(uint256 tradeVolume, uint256 currentPrice) {
        require(block.timestamp >= circuitBrokenUntil, "Circuit breaker active");

        // Reset window if expired
        if (block.timestamp > windowStart + VOLUME_WINDOW) {
            windowStart = block.timestamp;
            windowVolume = 0;
        }

        // Check volume spike
        windowVolume += tradeVolume;
        if (windowVolume > MAX_VOLUME_USD) {
            circuitBrokenUntil = block.timestamp + COOLDOWN_PERIOD;
            emit CircuitBroken("Volume threshold exceeded", block.timestamp, circuitBrokenUntil);
            revert("Circuit breaker: volume limit");
        }

        // Check price deviation
        if (lastKnownPrice > 0) {
            uint256 deviation = currentPrice > lastKnownPrice
                ? ((currentPrice - lastKnownPrice) * 10_000) / lastKnownPrice
                : ((lastKnownPrice - currentPrice) * 10_000) / lastKnownPrice;

            if (deviation > PRICE_CHANGE_THRESHOLD_BPS) {
                circuitBrokenUntil = block.timestamp + COOLDOWN_PERIOD;
                emit CircuitBroken("Price deviation exceeded", block.timestamp, circuitBrokenUntil);
                revert("Circuit breaker: price deviation");
            }
        }

        lastKnownPrice = currentPrice;
        _;
    }
}

The circuit breaker triggers when volume exceeds $10 million per hour or price moves more than 10% from the last known price. Both conditions are strong signals of flash loan manipulation. The 30-minute cooldown gives the team time to investigate before operations resume.

Access Control with Timelocks

For any function that moves significant value, timelocks create a mandatory delay between requesting an action and executing it. Flash loans are atomic — they cannot survive across blocks.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Timelock for high-value operations
contract TimelockController {
    uint256 public constant MIN_DELAY = 48 hours;
    uint256 public constant MAX_DELAY = 30 days;
    uint256 public constant GRACE_PERIOD = 14 days;

    mapping(bytes32 => uint256) public queuedTransactions;

    event TransactionQueued(bytes32 indexed txHash, uint256 executeAfter);
    event TransactionExecuted(bytes32 indexed txHash);
    event TransactionCancelled(bytes32 indexed txHash);

    function queueTransaction(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 delay
    ) external returns (bytes32) {
        require(delay >= MIN_DELAY, "Delay too short");
        require(delay <= MAX_DELAY, "Delay too long");

        bytes32 txHash = keccak256(abi.encode(target, value, data, block.timestamp));
        uint256 executeAfter = block.timestamp + delay;
        queuedTransactions[txHash] = executeAfter;

        emit TransactionQueued(txHash, executeAfter);
        return txHash;
    }

    function executeTransaction(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 queueTimestamp
    ) external payable returns (bytes memory) {
        bytes32 txHash = keccak256(abi.encode(target, value, data, queueTimestamp));

        uint256 executeAfter = queuedTransactions[txHash];
        require(executeAfter != 0, "Not queued");
        require(block.timestamp >= executeAfter, "Too early");
        require(block.timestamp <= executeAfter + GRACE_PERIOD, "Expired");

        delete queuedTransactions[txHash];

        (bool success, bytes memory result) = target.call{value: value}(data);
        require(success, "Execution failed");

        emit TransactionExecuted(txHash);
        return result;
    }
}

The 48-hour minimum delay means any governance action is visible on-chain for two days before it can execute. This gives the community and monitoring systems time to detect malicious proposals. Flash loans cannot bridge a 48-hour gap — they exist and die within a single transaction.


Testing for Flash Loan Vulnerabilities

When I audit a protocol for flash loan vulnerabilities, I follow a systematic process. Here are the specific patterns I look for and how you can test for them using Foundry:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, console2} from "forge-std/Test.sol";

contract FlashLoanVulnerabilityTest is Test {
    // Test 1: Can spot price be manipulated in a single transaction?
    function test_spotPriceManipulation() public {
        // Simulate a large swap that moves the price
        uint256 priceBefore = vulnerableOracle.getPrice(TOKEN);

        // Execute a massive swap (simulating flash loan capital)
        deal(TOKEN, address(this), 10_000_000e18);
        dex.swap(TOKEN, USDC, 10_000_000e18);

        uint256 priceAfter = vulnerableOracle.getPrice(TOKEN);

        // If price moved more than 5%, the oracle is manipulable
        uint256 deviation = priceBefore > priceAfter
            ? ((priceBefore - priceAfter) * 10_000) / priceBefore
            : ((priceAfter - priceBefore) * 10_000) / priceBefore;

        // This SHOULD fail — if it passes, the oracle is vulnerable
        assertGt(deviation, 500, "Oracle should be resistant to single-tx manipulation");
    }

    // Test 2: Can governance be hijacked in one transaction?
    function test_governanceFlashLoanResistance() public {
        // Simulate acquiring tokens and voting in the same block
        deal(GOV_TOKEN, address(this), 1_000_000e18);

        // Attempt to create a proposal — should use past block snapshot
        // If this succeeds with tokens acquired in the same block, vulnerable
        vm.expectRevert("Must hold tokens in previous block");
        governance.propose(keccak256("malicious"));
    }

    // Test 3: Can share price be inflated via donation?
    function test_sharePriceInflation() public {
        // Deposit minimal amount
        vault.deposit(1);

        // Donate large amount directly to vault (simulating flash loan)
        deal(TOKEN, address(vault), 1_000_000e18);

        // Check if share price is now inflated
        uint256 sharePrice = vault.convertToAssets(1e18);

        // Share price should not be manipulable via donation
        assertLt(sharePrice, 2e18, "Share price should resist donation attacks");
    }

    // Test 4: Invariant — protocol solvency across all operations
    function invariant_protocolSolvency() public view {
        uint256 totalDeposits = protocol.totalDeposits();
        uint256 totalBorrows = protocol.totalBorrows();
        uint256 reserves = TOKEN.balanceOf(address(protocol));

        // Total reserves should always cover deposits minus borrows
        assertGe(
            reserves + totalBorrows,
            totalDeposits,
            "Protocol insolvent"
        );
    }
}

Foundry's fuzz testing and invariant testing are the most effective tools I have found for catching flash loan vulnerabilities. The invariant tests run hundreds of randomized transaction sequences and verify that protocol invariants hold after every sequence. If a fuzz run finds a sequence where the protocol becomes insolvent, you have found a flash loan attack vector.


My Audit Process for Flash Loans

After performing security audits on DeFi protocols for the past three years, I have developed a systematic checklist for flash loan attack surfaces. I am sharing it here because I want protocol teams to start catching these issues before they reach an auditor.

Phase 1: Identify Attack Surface

I map every point where external data enters the protocol:

  • Price oracles: What is the source? Spot price or TWAP? What is the liquidity depth of the source pool?
  • Share price calculations: Does totalAssets include donatable tokens? Can the denominator be manipulated?
  • Governance: Can voting power be acquired and used in the same block?
  • Liquidations: Can an attacker trigger liquidations by manipulating the price feed?

Phase 2: Economic Analysis

For each attack surface, I calculate:

  • Cost of attack: How much capital is needed to move the price by X%? What is the flash loan fee?
  • Profit potential: How much can be extracted if the manipulation succeeds?
  • Liquidity analysis: What is the TVL of the oracle source pools? How deep is the order book?

If profit potential exceeds cost of attack, the vulnerability is exploitable.

Phase 3: Attack Simulation

I write Foundry test contracts that simulate complete attack sequences:

  1. Flash borrow maximum available capital
  2. Execute each identified manipulation vector
  3. Extract value from the vulnerable protocol
  4. Reverse manipulation
  5. Repay flash loan
  6. Assert that the attack was profitable

If the test passes — if the attacker ends with more tokens than they started — I file a critical finding.

Phase 4: Defense Verification

For every finding, I verify that the proposed mitigation actually works:

  • TWAP oracle with sufficient window (minimum 30 minutes)
  • Circuit breakers with appropriate thresholds
  • Governance snapshots using past block voting power
  • Share price calculations that resist donation attacks
  • Timelocks on high-value operations

I re-run the attack simulation against the mitigated code to confirm the attack no longer succeeds.

The Checklist

Here is the exact checklist I use. Pin this somewhere if you are building a DeFi protocol:

  • [ ] All price feeds use TWAP or Chainlink — never DEX spot prices
  • [ ] TWAP window is at least 30 minutes
  • [ ] Circuit breakers trigger on volume spikes and price deviations
  • [ ] Governance uses past-block snapshots for voting power
  • [ ] Minimum timelock of 48 hours on governance execution
  • [ ] Share price calculations use virtual offsets to resist inflation attacks
  • [ ] Liquidation thresholds account for oracle manipulation risk
  • [ ] All external calls are protected against reentrancy
  • [ ] Protocol invariants are tested with Foundry fuzz and invariant tests
  • [ ] Emergency pause mechanism exists and is tested

Key Takeaways

  1. Flash loans are not the vulnerability. They are the funding mechanism. The vulnerability is always in the protocol that gets exploited — manipulable oracles, instant governance, inflatable share prices.
  1. Spot prices are not oracles. If your protocol reads a DEX spot price and uses it for any lending, liquidation, or valuation logic, you are one transaction away from a flash loan exploit. Use TWAP oracles or Chainlink.
  1. Governance must be time-resistant. Voting power should come from past-block snapshots. Execution should have timelocks. Emergency functions should require multi-sig, not token voting.
  1. Defense in depth works. No single mitigation is sufficient. Combine TWAP oracles with circuit breakers, timelocks, and invariant testing. Each layer catches what the others miss.
  1. Test your invariants. Foundry fuzz testing is the closest thing we have to automated attack simulation. If your protocol invariants hold across thousands of random transaction sequences, they will likely hold against a flash loan attack.
  1. The economics must not pencil out. The ultimate defense is making the attack unprofitable. If manipulating the oracle costs more than the potential extraction, rational attackers will not bother.

Flash loan attacks have drained over a billion dollars from DeFi. Every single exploit I have studied was preventable with the patterns described in this article. The code is not complicated. The defenses are not novel. The protocols that get exploited simply did not implement them.

Do not be that protocol.


About the Author

I am Uvin Vindula — a Web3 engineer and security auditor based between Sri Lanka and the UK. I build production DeFi protocols and audit them for vulnerabilities including flash loan attack surfaces, reentrancy, access control issues, and economic exploits. I check for flash loan vectors in every single audit I perform because they remain the highest-impact attack class in DeFi.

If you are building a DeFi protocol and want a security audit before you deploy to mainnet, get in touch. I would rather find the vulnerability in your code before an attacker does.

Working on a Web3 or AI project?

Share
Uvin Vindula

Uvin Vindula

Web3 and AI engineer based in Sri Lanka and the UK. Author of The Rise of Bitcoin. Director of Blockchain and Software Solutions at Terra Labz. Founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.