← Back to Blog

Flash Loan Attack Patterns: How Aave, Balancer, and dYdX Loans Enable Protocol Exploits

2026-04-18 flash loan aave balancer dydx exploit defi attack pattern security

Flash loans do not introduce new attack categories — they remove the capital barrier to attacks that already exist. A protocol vulnerable to price oracle manipulation is vulnerable with or without flash loans. The flash loan just means an attacker with zero starting capital can carry out an exploit that would otherwise require $50M in liquid assets. This distinction matters for how you think about fixes: patching the flash loan entry point accomplishes nothing; patching the underlying design flaw does.

Aave V3, Balancer, and dYdX all offer uncollateralized loans that must be repaid within a single transaction. The loan amount is bounded only by the pool's liquidity — routinely tens or hundreds of millions of dollars. That capital, available atomically, fundamentally changes which attacks are economically viable.

This post covers six concrete attack patterns, each with a representative attack contract and its corresponding defensive fix.


1. Flash Loan Price Oracle Manipulation

The most common flash loan attack uses borrowed capital to move a spot price on a DEX, then interacts with a victim protocol that reads that spot price as truth.

Attack flow:

contract OracleManipulationAttack {
    IPool aave = IPool(AAVE_V3_POOL);
    IUniswapV2Pair pair = IUniswapV2Pair(USDC_ETH_PAIR);
    ILendingProtocol victim = ILendingProtocol(VICTIM);

    function attack() external {
        // 1. Borrow 50M USDC via Aave flash loan
        aave.flashLoanSimple(address(this), USDC, 50_000_000e6, "", 0);
    }

    function executeOperation(
        address asset, uint256 amount, uint256 premium, address, bytes calldata
    ) external returns (bool) {
        // 2. Swap USDC → ETH, inflating ETH spot price in the pair
        IERC20(USDC).transfer(address(pair), amount);
        pair.swap(0, ethOut, address(this), "");

        // 3. Victim reads pair.getReserves() as its oracle — sees inflated ETH price
        // Borrow maximum ETH-denominated assets against minimal collateral
        victim.borrowAtManipulatedPrice();

        // 4. Swap ETH back → USDC at near-market rate
        pair.swap(usdcOut, 0, address(this), "");

        // 5. Repay flash loan + premium
        IERC20(USDC).approve(address(aave), amount + premium);
        return true;
    }
}

The victim protocol loses the difference between the manipulated valuation and real market value. Real-world examples include the Harvest Finance exploit ($34M) and the Cheese Bank exploit.

Fix — TWAP oracle:

// Read a time-weighted average price, not a spot price
function getPrice(address token) internal view returns (uint256) {
    (uint256 price0Cumulative, uint256 price1Cumulative, uint32 blockTimestamp) =
        UniswapV2OracleLibrary.currentCumulativePrices(pair);
    uint32 timeElapsed = blockTimestamp - lastTimestamp;
    require(timeElapsed >= MIN_PERIOD, "TWAP: too soon"); // enforce e.g. 30-minute window
    uint224 price = uint224(
        (price0Cumulative - lastPrice0Cumulative) / timeElapsed
    );
    return uint256(price);
}

A 30-minute TWAP requires an attacker to sustain the manipulated price across many blocks — economically infeasible for any realistic attack budget.


2. Collateral Inflation Attack

Lending protocols that allow deposit and borrow in the same block can be attacked with a looping amplification strategy that uses a flash loan as the initial stake.

Attack flow:

contract CollateralInflationAttack {
    function attack(ILendingPool pool, address tokenA, address tokenB) external {
        uint256 borrowed = flashBorrow(tokenA, AMOUNT);

        // Loop: deposit A → borrow B → swap B → A → deposit more A
        for (uint i = 0; i < 5; i++) {
            pool.deposit(tokenA, borrowed, address(this), 0);
            uint256 mintedB = pool.borrow(tokenB, maxBorrowable(), address(this));
            borrowed = swapBForA(mintedB);
        }
        // Net result: far more A borrowed than the flash loan amount,
        // backed by collateral that was itself borrowed this block.
        repayFlashLoan();
    }
}

Each loop iteration uses the previous borrow as new collateral. A protocol that updates its internal accounting within the same block lets an attacker build a synthetic leverage tower with no real capital.

Fix — single-block borrow delay:

mapping(address => uint256) public lastDepositBlock;

function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referral) external {
    lastDepositBlock[onBehalfOf] = block.number;
    // ... normal deposit logic
}

function borrow(address asset, uint256 amount, ...) external {
    require(
        block.number > lastDepositBlock[msg.sender],
        "Cannot borrow in same block as deposit"
    );
    // ... normal borrow logic
}

Requiring at least one block between deposit and borrow collapses the entire attack: the flash loan must be repaid before the next block, so the loop cannot complete.


3. Flash Loan Governance Attack

The Beanstalk exploit ($182M, April 2022) showed that on-chain governance with snapshot voting is critically broken when flash loans exist. An attacker borrowed enough governance tokens to pass a proposal in a single transaction.

Attack flow:

contract GovernanceAttack {
    IGovernor gov = IGovernor(PROTOCOL_GOV);
    IBalancerVault balancer = IBalancerVault(BALANCER_VAULT);

    function attack(uint256 proposalId) external {
        // 1. Flash borrow governance tokens from Balancer
        balancer.flashLoan(address(this), tokens, amounts, abi.encode(proposalId));
    }

    function receiveFlashLoan(
        IERC20[] memory, uint256[] memory amounts, uint256[] memory, bytes memory data
    ) external {
        uint256 proposalId = abi.decode(data, (uint256));

        // 2. Cast vote with borrowed voting power
        gov.castVote(proposalId, 1 /* For */);

        // 3. If governance executes same block — drain treasury here
        gov.execute(proposalId);

        // 4. Repay Balancer (no fee on flash loans)
        repayBalancer(amounts);
    }
}

The root cause is that the governance contract reads token.balanceOf(voter) at vote time rather than using checkpointed historical balances.

Fix — use past votes checkpoint:

// OpenZeppelin Governor already supports this — make sure it's configured correctly
function _getVotes(
    address account,
    uint256 blockNumber,
    bytes memory
) internal view override returns (uint256) {
    // Read balance from BEFORE the current block — flash loans can't affect this
    return token.getPastVotes(account, blockNumber - 1);
}

// Also require a voting delay so tokens must be held before proposal creation
uint256 public constant VOTING_DELAY = 7200; // ~1 day in blocks

Using getPastVotes(block.number - 1) means any tokens acquired in the current block — including flash loan proceeds — carry zero voting power.


4. Flash Loan Sandwich of Protocol's Own Swap

Some protocols execute swaps internally as part of their normal operation — converting collected fees to a treasury token, rebalancing reserves, or settling funding payments. An attacker who can predict this swap can wrap a flash loan around it to extract value.

Attack flow:

contract ProtocolSandwichAttack {
    function attack(IVictimProtocol victim, IUniswapV2Pair pair) external {
        uint256 flash = flashBorrow(TOKEN_A, LARGE_AMOUNT);

        // Front-run: push price of TOKEN_A up before protocol swaps
        swapAForB(flash); // moves pool price

        // Protocol's internal swap now executes at a worse rate (protocol is the victim)
        victim.collectAndConvertFees(); // protocol swaps TOKEN_B → TOKEN_A at bad price

        // Back-run: sell TOKEN_B accumulated at inflated price
        swapBForA(ourBBalance);

        repayFlashLoan();
        // Profit = slippage extracted from protocol's forced swap
    }
}

The protocol acts as the price taker — it must execute the swap regardless of current conditions — and the attacker extracts the slippage as profit.

Fix — slippage protection on internal swaps:

function collectAndConvertFees() internal {
    uint256 amountIn = IERC20(feeToken).balanceOf(address(this));

    // Calculate expected output using a TWAP, not spot price
    uint256 expectedOut = twapOracle.getExpectedOutput(feeToken, treasuryToken, amountIn);
    uint256 minOut = expectedOut * 95 / 100; // 5% max slippage

    router.swapExactTokensForTokens(
        amountIn,
        minOut,          // revert if sandwich drives price past this threshold
        path,
        treasury,
        block.timestamp
    );
}

Any protocol that performs swaps must set its own amountOutMinimum. Leaving it at zero is equivalent to posting a standing invitation for sandwich attacks.


5. Leveraged Position Liquidation via Flash Loan

Liquidation bonuses create a profitable target. If an attacker can temporarily depress the collateral price of a leveraged position, they can trigger the liquidation themselves and collect the bonus.

Attack flow:

contract LiquidationAttack {
    function attack(address borrower, ILendingPool pool, address collateral) external {
        uint256 flash = flashBorrow(collateral, LARGE_AMOUNT);

        // 1. Depress collateral price by dumping into its primary pool
        dumpCollateralIntoPool(flash);
        // Pool price now reflects manipulated spot — if pool IS the oracle, position is now undercollateralized

        // 2. Liquidate the undercollateralized position, collecting bonus
        pool.liquidationCall(collateral, debtAsset, borrower, type(uint256).max, false);
        // Receive collateral + liquidation bonus (typically 5-15%)

        // 3. Restore price (optional — attacker already has bonus)
        buyCollateralFromPool();

        repayFlashLoan();
        // Net profit = liquidation bonus - flash loan fee - price impact costs
    }
}

This attack is particularly effective against protocols using AMM spot prices for liquidation eligibility checks.

Fix — multi-block TWAP and liquidation cooldown:

function isLiquidatable(address borrower) public view returns (bool) {
    uint256 collateralValue = twapOracle.getValue(collateral, MIN_TWAP_PERIOD);
    uint256 debtValue = twapOracle.getValue(debt, MIN_TWAP_PERIOD);
    return collateralValue * LTV_PRECISION < debtValue * liquidationThreshold;
}

mapping(address => uint256) public lastLiquidationBlock;

function liquidationCall(address borrower, ...) external {
    require(isLiquidatable(borrower), "Position is healthy");
    require(
        block.number >= lastLiquidationBlock[borrower] + LIQUIDATION_COOLDOWN,
        "Liquidation cooldown active"
    );
    lastLiquidationBlock[borrower] = block.number;
    // ... execute liquidation
}

A TWAP oracle cannot be moved within a single block. The liquidation cooldown prevents rapid repeated liquidations even if an attacker sustains price pressure across blocks.


6. Flash Loan-Enabled Arbitrage That Breaks Protocol Assumptions

AMMs and protocols with bonding curves frequently assume that the spread between their internal price and the external market price is bounded. Flash loans allow an attacker to temporarily maximize that spread and extract liquidity via arbitrage before the protocol can react.

Attack flow:

The attacker borrows a large amount, floods the AMM on one side to push its internal price far from the market, then exploits the protocol's own mechanisms — which are designed for small imbalances — to drain liquidity at the artificially favorable rate.

contract ArbitrageProtocolDrain {
    function attack(IAMMProtocol amm, address tokenA, address tokenB) external {
        uint256 flash = flashBorrow(tokenA, EXTREME_AMOUNT);

        // Push AMM internal price of tokenB way down (flood with tokenA)
        amm.swap(tokenA, tokenB, flash);
        // AMM internal: 1 tokenB = 0.1 tokenA (actual market: 1 tokenB = 1 tokenA)

        // Protocol's arbitrage incentive mechanism pays us to "rebalance"
        // We extract tokenB at 0.1x cost, sell externally at 1x
        uint256 tokenBReceived = amm.rebalance(); // protocol rewards arbitrageurs
        sellOnExternalMarket(tokenBReceived);

        repayFlashLoan();
        // Profit = (external price - internal price) * volume, minus fees
    }
}

Fix — rate-limit arbitrage paths and bound price impact:

uint256 public constant MAX_PRICE_IMPACT_BPS = 200; // 2% max per swap
uint256 public constant ARBITRAGE_WINDOW_BLOCKS = 10;
mapping(address => uint256) public lastArbitrageBlock;

modifier rateLimit(address caller) {
    require(
        block.number >= lastArbitrageBlock[caller] + ARBITRAGE_WINDOW_BLOCKS,
        "Rate limit: too many arbitrage calls"
    );
    lastArbitrageBlock[caller] = block.number;
    _;
}

function swap(address tokenIn, address tokenOut, uint256 amountIn)
    external
    rateLimit(msg.sender)
    returns (uint256 amountOut)
{
    uint256 priceBefore = getSpotPrice(tokenIn, tokenOut);
    amountOut = _swap(tokenIn, tokenOut, amountIn);
    uint256 priceAfter = getSpotPrice(tokenIn, tokenOut);

    uint256 impact = (priceBefore - priceAfter) * 10000 / priceBefore;
    require(impact <= MAX_PRICE_IMPACT_BPS, "Price impact too high");
}

Bounding price impact per swap forces an attacker to spread the manipulation across many transactions and many blocks — defeating the atomic nature of flash loan attacks.


What ContractScan Detects

ContractScan analyzes Solidity source and bytecode for patterns that enable these exploits. The table below maps each attack pattern to its detection method and assigned severity.

Vulnerability Detection Method Severity
Flash Loan Price Oracle Manipulation Identifies getReserves() / slot0() spot price reads in valuation paths without TWAP guards Critical
Collateral Inflation (Same-Block Borrow) Detects missing block.number checks between deposit and borrow state transitions High
Governance Flash Loan Voting Flags balanceOf usage in vote weight calculation instead of getPastVotes with historical block Critical
Protocol Swap Sandwich Finds internal swap calls with zero or unchecked amountOutMinimum / minAmountOut High
Liquidation Oracle Manipulation Detects spot price oracles used in liquidation eligibility logic without time-weighting Critical
Unbounded AMM Arbitrage Identifies missing price impact bounds and absent rate-limiting on swap or rebalance entry points Medium

All six detectors run automatically on every scan. Results include the vulnerable code path, the manipulable state variable, and a recommended remediation pattern tailored to the specific protocol architecture.


Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →