On August 27, 2026, an attacker pumped the price of a thinly traded governance token called MAMO by nearly eightfold, posted it as collateral on Moonwell’s Base lending markets, and walked away with $8.7 million. No smart contract bug was involved. The protocol’s oracle simply believed the price it was fed. Seven months earlier, Makina Finance lost $4.2 million the same way: a flash loan inflated a Curve pool, the protocol’s share price followed, and the attacker cashed out in one transaction. TRM Labs reported on September 2, 2026 that price manipulation exploits tied to flash loans hit a record high this year, and the OWASP Smart Contract Security project now tracks the pattern under its own classification, SC04:2026.

This tutorial walks through building a Foundry test suite that simulates these attacks against your own contracts before an attacker does it for you. You’ll fork mainnet state, script the borrow-distort-extract sequence that defines almost every flash loan exploit this year, and then harden a lending contract with TWAP pricing, circuit breakers, and per-block caps until the same test fails to profit. By the end you’ll have a working repository you can drop into any Solidity project.

Why Flash Loan Oracle Attacks Are DeFi’s Biggest Threat in 2026

A flash loan lets anyone borrow millions of dollars with zero collateral, as long as the loan is repaid within the same blockchain transaction. That single property turns a well-funded attacker and a broke one into equals for about twelve seconds. The borrowed capital doesn’t need to leave the transaction to do damage. It just needs to move a price that some other contract trusts.

2026 gave the pattern plenty of fresh case studies. Kelp DAO lost $292 million in April after an attacker exploited a delayed oracle update and inflated collateral values through ERC-4626 vault pricing, according to a breakdown published by security firm Soken. Venus Protocol on ZKsync lost $717,000 in February when a $4 million Aave flash loan pushed the wUSDM exchange rate from 1.06 to 1.7, enough to trigger a profitable self-liquidation. On Stellar, an attacker targeted YieldBlox’s Reflector price oracle in the same month, and Blend Protocol separately lost $10 million to oracle manipulation, according to a Q1 2026 hack report from Blockeden that put total DeFi losses for the quarter at $169 million.

The mechanics repeat often enough that a dev.to writeup on the Makina Finance exploit distilled them into three stages: borrow, distort, extract. Borrow temporary capital with no collateral. Distort a price feed that some contract reads as ground truth. Extract more value than you put in, using the distorted price to your advantage, then repay the loan and keep the spread. Every step happens inside one transaction, so there’s no window for a human to intervene.

Static audits catch some of this. They rarely catch all of it, because the vulnerability usually isn’t a bug in your code, it’s an assumption your code makes about a price it doesn’t control. That’s why fork-based simulation testing matters: you’re not looking for a broken require statement, you’re proving whether your contract can be pushed into an unprofitable state by an attacker with borrowed capital and one transaction.

How the Attack Works: Borrow, Distort, Extract

Before writing any test code, it helps to walk through the attack shape you’re simulating. Picture a lending protocol that accepts a governance token as collateral and prices it using the spot rate from a single automated market maker pool, the same setup Moonwell had in August.

  • Borrow: the attacker takes a flash loan of a stable asset, often from Aave, Balancer, or Morpho, with no collateral posted.
  • Distort: the attacker dumps that stable asset into a thin liquidity pool holding the collateral token, spiking its spot price far above fair value.
  • Exploit: the attacker deposits the now-overpriced token as collateral on the lending protocol and borrows against the inflated value.
  • Extract: the attacker withdraws the borrowed funds, reverses the pool trade to restore the price, and repays the flash loan, keeping the difference.

Everything above happens in a single block. There’s no time for a keeper bot, a governance vote, or a human operator to react. That’s precisely what makes fork testing the right tool: you need to prove a contract’s behavior under an atomic, adversarial sequence, and unit tests written against isolated mocks won’t expose it. A test that mocks the price oracle to always return a fixed number will pass every time, because it never lets the price move the way a real pool would.

Prerequisites and Tool Versions

You’ll need a working Solidity toolchain and access to an archive RPC endpoint for forking. Foundry ships fast-moving releases, so pin your installed versions with the commands below rather than trusting a number printed in a blog post from months ago.

ToolPurposeCheck installed version with
Foundry (forge, cast, anvil)Fork testing, fuzzing, invariant testingforge --version
SlitherStatic analysis, cross-check for logic bugsslither --version
Node.jsRunning auxiliary scripts, optionalnode --version
GitCloning the sample contracts and dependenciesgit --version
Archive RPC endpointForking mainnet or L2 state at a specific blockAlchemy, Infura, or a self-hosted archive node

You don’t need an exact version number memorized. Foundry updates through foundryup often enough that hardcoding one in a guide would be stale within weeks. Run the version checks above right before you start, and record the output in your test repository’s README so anyone who clones it later can reproduce your results exactly.

Step 1: Install Foundry and Scaffold the Project

Install Foundry through its official installer, then initialize a fresh project. If you already have Foundry, run foundryup to pull the latest build before you start, since fork-testing cheatcodes get bug fixes regularly.

curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init flash-loan-attack-lab
cd flash-loan-attack-lab
forge --version

Inside the new project, create a foundry.toml that points at a fork RPC and pins the Solidity version you’re targeting. Keep your RPC key out of version control by loading it from an environment variable.

[profile.default]
src = "src"
test = "test"
out = "out"
libs = ["lib"]
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 200

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
base = "${BASE_RPC_URL}"

Export your RPC URL in the shell before running any fork test, for example export MAINNET_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/your-key". Every fork command in this tutorial reads from that variable.

Step 2: Fork Mainnet State With Forge

Fork testing replays real, deployed contract state inside a local sandbox. Instead of deploying mock tokens and mock pools, you attach to the actual Uniswap pool, the actual Aave pool, and the actual price feeds your contract depends on in production. This is the detail that separates a meaningful flash loan simulation from a toy test: the liquidity depth, the slippage curve, and the gas costs are all real.

forge test --match-test testFlashLoanOracleAttack \
  --fork-url $MAINNET_RPC_URL \
  --fork-block-number 21500000 \
  -vvvv

Pinning a specific --fork-block-number matters as much as picking the right RPC. Without it, forge grabs whatever the latest block happens to be each time you run the suite, and a test that passes today can start failing tomorrow because pool liquidity shifted underneath it. Pick a block number close to when you’re testing, write it down, and only bump it deliberately.

Step 3: Build a Minimal Vulnerable Lending Contract

To have something concrete to attack, scaffold a stripped-down lending contract that prices collateral off a single AMM pool’s spot rate, the exact pattern behind the Moonwell exploit. Save this as src/VulnerableLending.sol.

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IUniswapV2Pair} from "./interfaces/IUniswapV2Pair.sol";

contract VulnerableLending {
    IERC20 public immutable collateralToken;
    IERC20 public immutable borrowToken;
    IUniswapV2Pair public immutable pricePool;

    mapping(address => uint256) public collateralBalance;

    constructor(address _collateral, address _borrow, address _pool) {
        collateralToken = IERC20(_collateral);
        borrowToken = IERC20(_borrow);
        pricePool = IUniswapV2Pair(_pool);
    }

    function spotPrice() public view returns (uint256) {
        (uint112 reserve0, uint112 reserve1,) = pricePool.getReserves();
        return (uint256(reserve1) * 1e18) / uint256(reserve0);
    }

    function depositCollateral(uint256 amount) external {
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateralBalance[msg.sender] += amount;
    }

    function borrow(uint256 amount) external {
        uint256 collateralValue = (collateralBalance[msg.sender] * spotPrice()) / 1e18;
        require(amount <= collateralValue, "insufficient collateral");
        borrowToken.transfer(msg.sender, amount);
    }
}

The bug sits in spotPrice(). It reads reserves directly from a single pool with no time-weighting and no sanity check against a second source. That single line is what your attack test will exploit.

Step 4: Simulate the Flash Loan Borrow

Now write the attacker contract. It implements Aave's flash loan receiver interface so the Aave pool can call back into it mid-transaction, the same mechanism real attackers used against Makina Finance and Venus Protocol.

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

import {IPool} from "@aave/interfaces/IPool.sol";
import {IFlashLoanSimpleReceiver} from "@aave/interfaces/IFlashLoanSimpleReceiver.sol";
import {VulnerableLending} from "../src/VulnerableLending.sol";

contract FlashLoanAttacker is IFlashLoanSimpleReceiver {
    IPool public immutable aavePool;
    VulnerableLending public immutable target;
    address public immutable pricePool;

    constructor(address _aavePool, address _target, address _pricePool) {
        aavePool = IPool(_aavePool);
        target = VulnerableLending(_target);
        pricePool = _pricePool;
    }

    function launch(address stableAsset, uint256 amount) external {
        aavePool.flashLoanSimple(address(this), stableAsset, amount, "", 0);
    }

    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address,
        bytes calldata
    ) external returns (bool) {
        // Stage 1: distort (dump borrowed stable asset into the thin pool)
        _distortPrice(asset, amount);

        // Stage 2: extract (borrow against the now-inflated collateral value)
        target.borrow(target.collateralBalance(address(this)));

        // Stage 3: repay the flash loan plus Aave's premium
        IERC20(asset).approve(address(aavePool), amount + premium);
        return true;
    }

    function _distortPrice(address asset, uint256 amount) internal {
        // swap logic against pricePool omitted for brevity, see repo
    }
}

Keep the swap logic in its own internal function. It'll get long once you account for real pool interfaces, and separating it makes the borrow-distort-extract stages easy to read in isolation, which matters when you're debugging why an assertion failed at 2am.

Step 5: Model the Price Distortion

The distortion step is where fork testing earns its keep. Against a mock pool, a price swing is just a number you set. Against a forked real pool, the swing has to come from an actual swap, which means it's bounded by real liquidity depth and real slippage, exactly like it would be on mainnet.

function testFlashLoanOracleAttack() public {
    uint256 attackerBalanceBefore = stableAsset.balanceOf(attackerEOA);

    vm.startPrank(attackerEOA);
    attacker.launch(address(stableAsset), FLASH_LOAN_AMOUNT);
    vm.stopPrank();

    uint256 attackerBalanceAfter = stableAsset.balanceOf(attackerEOA);
    uint256 profit = attackerBalanceAfter - attackerBalanceBefore;

    console.log("Attacker profit:", profit);
    assertEq(profit, 0, "contract should not be profitable to attack");
}

Run this against the vulnerable contract from Step 3 and the assertion should fail, printing a nonzero profit figure in the console output. That failure is the point. You want to see the exploit succeed against the unpatched contract before you can prove your fix actually closes it.

Step 6: Execute the Extraction and Assert Profit

Run the test with verbose tracing so you can read the full call stack, including every internal swap and transfer.

forge test --match-test testFlashLoanOracleAttack -vvvv --fork-url $MAINNET_RPC_URL --fork-block-number 21500000

Example output against the vulnerable contract:

Ran 1 test for test/FlashLoanAttack.t.sol:FlashLoanAttackTest
[FAIL: contract should not be profitable to attack] testFlashLoanOracleAttack() (gas: 812,441)
Logs:
  Attacker profit: 41230000000000000000000

Test result: FAILED. 0 passed; 1 failed; 0 skipped

That FAILED result is expected and correct at this stage. It's telling you the vulnerable contract is exploitable, worth roughly 41,230 units of the borrow token in this run. Screenshot or log that number. It becomes your benchmark for confirming the fix in the next step actually works, rather than just assuming it does.

Step 7: Add a TWAP Oracle Guard and Re-Test

The fix that shows up across nearly every postmortem this year, including the Soken analysis of the Kelp DAO hack, is a time-weighted average price with a bounded deviation check against the spot rate. Replace the single-block spotPrice() call with one that reads a TWAP and rejects trades if the spot price has moved too far from it in one block.

function safePrice() public view returns (uint256) {
    uint256 twap = oracle.consultTwap(address(pricePool), TWAP_WINDOW);
    uint256 spot = spotPrice();

    uint256 deviation = spot > twap
        ? ((spot - twap) * 1e18) / twap
        : ((twap - spot) * 1e18) / twap;

    require(deviation <= MAX_DEVIATION_BPS * 1e14, "price deviation too high");
    return twap;
}

Swap spotPrice() for safePrice() inside the borrow() function, then re-run the exact same attack test from Step 6 without changing a single line of the attacker contract. If the guard works, the flash loan still executes, the pool still gets distorted, but the require now reverts the borrow call, and your assertion of zero profit passes.

Step 8: Fuzz and Invariant-Test With Foundry

A single hardcoded attack size only proves the guard blocks that one scenario. Foundry's fuzzer and invariant tester check a much wider range automatically, including attack sizes and pool states you wouldn't think to test by hand.

function testFuzz_AttackNeverProfitable(uint256 loanAmount) public {
    loanAmount = bound(loanAmount, 1e18, 50_000_000e18);

    uint256 balanceBefore = stableAsset.balanceOf(attackerEOA);
    vm.prank(attackerEOA);
    try attacker.launch(address(stableAsset), loanAmount) {
        uint256 balanceAfter = stableAsset.balanceOf(attackerEOA);
        assertLe(balanceAfter, balanceBefore, "attack should never be net profitable");
    } catch {
        // revert is an acceptable outcome, the guard rejected the trade
    }
}

Run it with a higher run count than the default to get real coverage: forge test --match-test testFuzz_AttackNeverProfitable --fuzz-runs 5000 --fork-url $MAINNET_RPC_URL. Fuzzing across loan sizes catches edge cases at the boundary of your deviation threshold that a single fixed-value test would miss entirely.

Step 9: Cross-Check With Slither Static Analysis

Fork tests prove behavior under one attack shape. Static analysis catches classes of bugs your test suite might not think to model yet. Run Slither against the same contracts and treat any oracle-related or reentrancy-related finding as a lead worth turning into its own fork test.

pip install slither-analyzer
slither src/VulnerableLending.sol --print human-summary
slither src/VulnerableLending.sol --detect reentrancy-eth,unchecked-transfer,timestamp

Slither won't flag "your oracle can be manipulated by a flash loan" as a standalone finding, because that's a design-level economic assumption rather than a syntax-level bug. What it will flag is a missing reentrancy guard around external calls, or unchecked return values on token transfers, both of which frequently show up alongside oracle bugs in the same vulnerable contracts. Use it as a companion pass, not a replacement for the fork attack simulation.

Step 10: Add Circuit Breakers and Per-Block Caps

TWAP guards close the single-transaction attack. They don't fully stop an attacker willing to manipulate a price gradually across several blocks, which is a slower but real variant. Add a hard cap on how much value can move through the protocol per block, and a pause switch that a monitoring bot or multisig can trip if the cap gets approached.

uint256 public borrowedThisBlock;
uint256 public lastBlockChecked;
uint256 public constant MAX_BORROW_PER_BLOCK = 500_000e18;

modifier rateLimited(uint256 amount) {
    if (block.number != lastBlockChecked) {
        borrowedThisBlock = 0;
        lastBlockChecked = block.number;
    }
    require(borrowedThisBlock + amount <= MAX_BORROW_PER_BLOCK, "block cap exceeded");
    borrowedThisBlock += amount;
    _;
}

Write a fork test that pushes borrow volume right up to MAX_BORROW_PER_BLOCK and confirms the next call in the same block reverts. This is also where you test that legitimate users below the cap still get served normally, since an overly aggressive cap just trades one failure mode for another.

Calculating Attacker Economics: When Does an Exploit Actually Pay Off

A test that shows a contract can be manipulated isn't automatically proof of a real risk. The manipulation also has to be profitable once you subtract the flash loan premium, gas costs, and any slippage the attacker eats while reversing their price-distortion trade. Skipping this check leads to false positives that waste a team's time chasing exploits nobody would actually execute.

Extend the attack test from Step 6 to log the full cost breakdown alongside the profit figure. Aave's flash loan premium currently runs a small fraction of a percent per loan, small enough that it rarely blocks an attack on its own, but it adds up on the largest loan sizes your fuzz test explores in Step 8. Gas costs matter more on Ethereum mainnet during periods of network congestion than they do on a low-fee L2 like Base or ZKsync, which partly explains why so many of 2026's oracle exploits, including Moonwell and Venus Protocol, landed on cheaper chains rather than mainnet.

function testFlashLoanEconomics() public {
    uint256 gasStart = gasleft();
    uint256 balanceBefore = stableAsset.balanceOf(attackerEOA);

    vm.prank(attackerEOA);
    attacker.launch(address(stableAsset), FLASH_LOAN_AMOUNT);

    uint256 gasUsed = gasStart - gasleft();
    uint256 grossProfit = stableAsset.balanceOf(attackerEOA) - balanceBefore;
    uint256 flashLoanPremium = (FLASH_LOAN_AMOUNT * 5) / 10_000; // 0.05%
    uint256 estimatedGasCostInAsset = gasUsed * tx.gasprice;

    console.log("Gross profit:", grossProfit);
    console.log("Flash loan premium:", flashLoanPremium);
    console.log("Estimated gas cost:", estimatedGasCostInAsset);
}

Run this variant across several loan sizes and chart net profit against loan amount. If the curve stays negative across the whole range you tested, the manipulation is real but not economically attractive at current gas and premium levels, worth documenting rather than urgently patching. If it turns positive anywhere in the range, especially near the smaller loan sizes an attacker could execute cheaply, treat it exactly like the Moonwell and Makina Finance cases and fix it before shipping.

Modeling Governance and Multi-Step Exploit Variants

Not every 2026 flash loan attack stopped at a single price feed. In February, the SOF and LAXO tokens on BSC were hit by a multi-step attack that chained a flash loan into both price manipulation and a governance exploit in the same block, according to a forum writeup published on Blockeden. The attacker used borrowed capital to temporarily acquire outsized voting power alongside a distorted price, then abused a flawed token burn mechanism, all inside one atomic transaction. Rhea Finance on NEAR Protocol suffered a related pattern in April, where oracle manipulation was combined with fake collateral postings rather than a governance vote.

If your protocol has on-chain governance with a token-weighted vote, extend the attacker contract from Step 4 with a fourth stage that calls into your governance module between the distort and extract steps. Assert that voting power calculated mid-transaction, using a flash-loaned token balance, cannot pass a proposal or trigger a parameter change. Most governance modules solve this with a snapshot mechanism that reads token balances from a block prior to the proposal, rather than the current block, which makes a flash loan useless for vote manipulation since the attacker never held the tokens at the snapshot block. Confirm your own governance contract actually uses a historical snapshot and doesn't fall back to balanceOf() at execution time as a shortcut anywhere in the codebase.

Step 11: Wire the Suite Into CI

A flash loan attack test that only runs on your laptop protects nobody. Add it to your CI pipeline so every pull request that touches pricing logic gets re-checked against the exploit automatically.

name: flash-loan-attack-suite
on: [pull_request]
jobs:
  fork-attack-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: foundry-rs/foundry-toolchain@v1
      - run: forge test --match-path "test/FlashLoanAttack.t.sol" -vv
        env:
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
      - run: pip install slither-analyzer && slither src/ --fail-high

Store your RPC URL as a repository secret, never in the workflow file. Pin the fork block number as an environment variable too, so CI runs stay deterministic instead of drifting with whatever block happens to be latest each run.

Step 12: Document Findings and Ship

Write up what you tested, what failed before the fix, and what passed after, in plain language a non-Solidity reviewer could follow. If your protocol has a bug bounty program through Immunefi or a similar platform, this writeup doubles as the basis for a disclosure report if you ever find a real issue instead of a synthetic one. Keep the attack contracts in your repository under a clearly marked test/attacks/ directory so future contributors know they're intentional exploit simulations, not code that shipped by accident.

Complete Working Project Structure

Once you've worked through all twelve steps, your repository should look like this:

flash-loan-attack-lab/
├── foundry.toml
├── src/
│   ├── VulnerableLending.sol
│   ├── SafeLending.sol
│   └── interfaces/
│       └── IUniswapV2Pair.sol
├── test/
│   ├── FlashLoanAttack.t.sol
│   ├── FuzzAttack.t.sol
│   └── attacks/
│       └── FlashLoanAttacker.sol
├── .github/
│   └── workflows/
│       └── flash-loan-attack-suite.yml
└── README.md

The two-contract split between VulnerableLending.sol and SafeLending.sol is deliberate. Keeping both in the repository, with the same attack test run against each, gives every new team member a runnable before-and-after comparison instead of a paragraph of prose asking them to trust that the fix works.

Clone the structure above into any existing Solidity project rather than starting from scratch. The attacker contracts and fork tests don't need to touch your production deployment scripts, they only need read access to your pricing and collateral logic. Teams that add this lab as a subdirectory inside their main repository, instead of a separate standalone project, tend to keep it updated for longer, since it shows up in the same pull request review as the contract changes it's meant to guard.

Common Pitfalls to Avoid

  • Testing against mocked oracles instead of forked pools. A mock that returns a fixed price can never demonstrate a manipulation attack, because the price never actually moves the way a real pool would under a large swap.
  • Leaving the fork block number floating. Not pinning --fork-block-number makes your test results non-reproducible and can turn a passing CI run into a flaky one as mainnet liquidity shifts.
  • Only testing the happy-path attack size. Attackers don't announce their loan size in advance. Fuzzing across a wide range, as in Step 8, catches thresholds you'd otherwise miss by hand.
  • Adding a TWAP guard but forgetting the fallback path. Some contracts have a secondary code path, like an emergency withdrawal function, that still reads the raw spot price and bypasses the fix entirely.
  • Setting the deviation threshold too loosely. A guard that allows 20% deviation per block still leaves room for a gradual, multi-block manipulation, which is why the rate limiting in Step 10 matters alongside the TWAP fix.
  • Running Slither once and calling it done. Static analysis needs to run on every change to pricing logic, not just before the initial launch, since new code paths can reopen an oracle assumption that was previously safe.

Troubleshooting Guide

Here are the errors you're most likely to hit while building this suite, and what they usually mean.

  • "Fork not found" or connection timeout on forge test: your RPC URL is missing, expired, or rate-limited. Confirm it with cast block-number --rpc-url $MAINNET_RPC_URL before rerunning the test.
  • Attack test passes even against the unpatched contract: your distortion swap size is too small relative to the pool's liquidity depth at the pinned block. Increase the flash loan amount or pick a fork block where the target pool holds less liquidity.
  • "Stack too deep" compiler error: Solidity's local variable limit is easy to hit inside a multi-stage attack function. Split the borrow, distort, and extract logic into separate internal functions as shown in Step 4.
  • Flash loan callback reverts with no clear reason: confirm your attacker contract approved the flash loan pool for at least amount + premium, not just amount. Missing the premium is the single most common cause of a silent revert here.
  • Fuzz test runs but never finds a failing case: your bound() range may be too narrow. Widen the fuzzed loan amount range and increase --fuzz-runs to at least 5,000.
  • Slither hangs or times out on a large codebase: scope it to a single file or directory first with slither src/VulnerableLending.sol rather than running it against the entire project on every save.
  • CI passes locally but fails in GitHub Actions: almost always a missing or misnamed repository secret for the RPC URL. Double check the secret name matches exactly what's referenced in the workflow YAML.
  • TWAP guard rejects legitimate price movements during real volatility: your MAX_DEVIATION_BPS threshold is too tight for the asset's normal volatility. Back-test the threshold against several months of the pool's historical price data before finalizing it.

Advanced Tips for Production Teams

Once the core suite is running, a few extensions raise the bar further. Add a second, independent oracle source, such as a Chainlink feed alongside your on-chain TWAP, and require both to agree within a tolerance before honoring a price. Multi-source validation is exactly what the Soken analysis of the Kelp DAO hack recommends as the strongest available defense, since manipulating two independent price sources simultaneously is far harder than manipulating one.

Consider running your attack suite against forked state from multiple blocks spread across a period of high and low liquidity, not just one snapshot. Liquidity in smaller pools swings by season and by market conditions, and a guard that holds at one fork block can behave differently at another. For protocols with governance tokens used as collateral, specifically model the Moonwell scenario: a thin-liquidity governance token pumped through a small pool, deposited as collateral, and borrowed against, since that pattern accounted for one of the largest losses of the year without touching a single line of exploitable Solidity code.

Finally, treat the attack contracts you write here as living documentation. Every time a new exploit pattern shows up in a postmortem, whether it's a governance-token pump, a delayed-oracle-update trick like Kelp DAO's, or a self-liquidation loop like Venus Protocol's, add it as a new test case rather than a one-off manual check. The OWASP SC04:2026 classification exists precisely because these patterns repeat across protocols, and a growing test library is more useful than a single point-in-time audit.

2026 Flash Loan and Oracle Attacks at a Glance

ProtocolDateLossChainAttack mechanism
Kelp DAOApril 2026$292MEthereumDelayed oracle update, inflated ERC-4626 vault pricing
MoonwellAug 27, 2026$8.7MBaseThin-liquidity governance token pumped, posted as collateral
Makina FinanceJan 20, 2026$4.2MEthereumFlash loan-inflated Curve pool skewed share price
Blend ProtocolJan 2026$10MStellarOracle manipulation via Reflector price feed
Venus ProtocolFeb 27, 2026$717KZKsyncwUSDM exchange rate inflated, triggered self-liquidation
YieldBlox (Blend)Feb 2026UndisclosedStellarVWAP oracle manipulation on USTRY/USDC pair

Figures above are drawn from post-incident analyses published by Soken, QuillAudits, Blockeden, AInvest, and Tech Times through August 2026. Amounts reflect the value reported at the time of each incident.

TWAP vs Multi-Source vs Circuit Breaker: Which Defense Fits Your Protocol

None of the defenses in this tutorial are mutually exclusive, and most production protocols in 2026 run more than one at once. The table below breaks down what each one actually stops, and where it falls short on its own.

DefenseStops single-block attacksStops multi-block manipulationImplementation cost
TWAP oracle with deviation capYesPartiallyLow to moderate
Multi-source oracle (e.g. TWAP + Chainlink)YesYesModerate
Per-block borrow/withdrawal capYesNoLow
Circuit breaker / pause switchReactive, not preventiveReactive, not preventiveLow
Withdrawal delay windowYesYesModerate, adds UX friction

A TWAP guard alone stops the exact single-transaction pattern behind Makina Finance and Venus Protocol. Layering in a second independent price source closes the gap a determined attacker could exploit by manipulating the underlying pool across several blocks instead of one. Circuit breakers won't prevent a first attack, since they need a human or a bot to notice and trigger them, but they cap the damage of a second attempt against the same weakness.

Frequently Asked Questions

What's the difference between a flash loan attack and a flash loan?

A flash loan itself is a legitimate DeFi primitive, an uncollateralized loan that must be repaid within the same transaction it's taken out in. It's used constantly for arbitrage and liquidations. A flash loan attack is when that same mechanism gets used to temporarily distort a price feed or governance vote that another contract mistakenly trusts as accurate.

Can Foundry actually simulate a real flash loan against live pool liquidity?

Yes. Forking mainnet or an L2 state at a specific block, as shown in Step 2, attaches your local test environment to the real deployed contracts, including their actual liquidity and reserves at that block. Swaps and price impacts in your test behave exactly as they would on-chain.

Does a TWAP oracle completely eliminate flash loan attack risk?

It closes the single-transaction version of the attack, since a TWAP can't be meaningfully moved within one block. It doesn't fully stop a patient attacker willing to manipulate a price gradually across several blocks, which is why pairing TWAP pricing with a second independent oracle source or a withdrawal delay, as covered in the advanced tips section, is the stronger combination.

Why did Moonwell lose $8.7 million without any smart contract bug?

According to Tech Times' reporting on the incident, the attacker pumped the price of an illiquid governance token nearly eightfold in a thin AMM pool with no TWAP guard, then posted that token as collateral to borrow against its inflated value. The contract logic executed exactly as written. The flaw was an economic and pricing assumption, not a coding error, which is exactly the category of risk this tutorial's fork tests are built to catch.

Do I need mainnet ETH to run these fork tests?

No. Forking creates a local copy of chain state that you can manipulate freely, including minting yourself test tokens with Foundry's deal() cheatcode. You need an archive RPC endpoint to read historical state, but no real funds or gas are spent.

How often should this attack suite run in CI?

At minimum, on every pull request that touches pricing, collateral, or oracle logic, as configured in Step 11. Many teams also schedule a weekly full run against a fresh fork block, since pool liquidity conditions change over time and a guard that held last month might need a wider or narrower threshold today.

Is Slither a replacement for fork-based attack testing?

No, the two are complementary. Slither is a static analyzer that flags known vulnerability patterns in your source code without executing anything. Fork testing actually runs an attack against forked, real-world state to prove or disprove exploitability under an adversarial scenario. Use Slither first as a fast pass, then fork-test anything involving pricing or collateral logic.

What's a reasonable deviation threshold for a TWAP guard?

There's no universal number, since it depends on the asset's normal volatility and the pool's typical liquidity depth. Back-test candidate thresholds against several months of that specific pool's historical price swings, as noted in the troubleshooting section, rather than copying a value from another protocol's contract.

Should small protocols with limited engineering resources still build this test suite?

Yes, arguably more urgently than larger ones. Moonwell, Venus Protocol, and YieldBlox were all established protocols with existing security processes, and all three still lost funds to oracle assumptions their teams hadn't stress-tested under an adversarial scenario. A smaller team with a single lending market and one price feed can build the core version of this suite, Steps 1 through 7, in an afternoon, and that afternoon of work directly targets the exact failure mode responsible for most of 2026's largest DeFi losses.