Back to Blog 
Uniswap V3 —
The

In this article: the four-phase pattern behind every flash loan exploit, side-by-side mechanics of every major flash loan provider, the seven vulnerability classes flash loans amplify, eight case studies at 223M scale, and the defenses that actually hold.
A flash loan turns a $0 attacker into a billion-dollar whale for the duration of one EVM transaction, then settles the books before the next block confirms.
Cetus lost 27M in September 2024 to a reentrancy that flash-borrowed reward tokens made decisive. KyberSwap Elastic lost $48M in November 2023 to a 1-wei tick precision asymmetry that flash-borrowed swap volume made reachable.
Flash loans are not the bug in any of these incidents. They are the amplifier that makes existing flaws — a spot price oracle, a donation-attackable vault, a misordered state update, a precision-loss tick boundary — pay out at full protocol scale instead of at attacker capital scale.
This guide walks the four-phase anatomy every flash loan exploit follows, the provider mechanics auditors should recognize at sight, the vulnerability classes that consistently show up downstream of a
flashLoan() call, and the case studies that prove the pattern at $200M+ scale. We close with the defenses that work — and the anti-patterns that keep showing up in production code.What flash loans actually are

A flash loan is an uncollateralized loan whose repayment is enforced not by collateral or credit, but by the atomicity of the EVM transaction itself.
The lender transfers tokens to the borrower's contract, calls back into that contract with arbitrary calldata, and at the end of that callback verifies that its balance has been restored plus any fee. If the post-condition fails, the entire transaction — borrow, every operation downstream of it, and any state changes anywhere in the call graph — reverts. There is no default. There is only success or revert.
That single property collapses the capital requirement for many DeFi attacks from "have $500M" to "pay 50,000 gas for the call." The attacker never owns the capital. They route it through a sequence of contracts long enough to extract a delta, and the lender is repaid before the block closes.
Flash loans amplify, they don't originate. The bugs they monetize are price oracles, share-accounting math, governance vote-counting, share inflation, reentrancy, and rounding. A flash loan turns a 0.5% protocol error on a 250K profit per cycle, executed 30 times in a single transaction. For the foundational AMM mechanics that many of these attacks abuse, see our AMM security foundations part 1 and part 2.
Recognize each provider at sight
Auditors should know each major flash loan provider's interface, fee schedule, and callback shape on muscle memory. The calling contract structure is the most reliable indicator that you are looking at flash-loan-augmented code.
Aave V3 — the workhorse
Aave exposes both
flashLoanSimple (single asset) and flashLoan (multi-asset, supports opening debt instead of repaying):1// Pool.sol (Aave V3)2function flashLoanSimple(3 address receiverAddress,4 address asset,5 uint256 amount,6 bytes calldata params,7 uint16 referralCode8) public virtual override;910function flashLoan(11 address receiverAddress,12 address[] calldata assets,13 uint256[] calldata amounts,14 uint256[] calldata interestRateModes,15 address onBehalfOf,16 bytes calldata params,17 uint16 referralCode18) public virtual override;1920// IFlashLoanSimpleReceiver21function executeOperation(22 address asset,23 uint256 amount,24 uint256 premium,25 address initiator,26 bytes calldata params27) external returns (bool);
Total premium is currently 0.05% (
FLASHLOAN_PREMIUM_TOTAL), of which 4 bps goes to the protocol treasury and the remainder accrues to liquidity providers. Aave V2 charged 0.09%. The receiver must approve the Pool for amount + premium before executeOperation returns; the Pool then pulls the funds via transferFrom. For the surrounding pool internals, see our Aave V3 Pool.sol walkthrough.dYdX Solo Margin — the original "free" flash loan
dYdX never built a flash loan product. The community discovered that
SoloMargin.operate() accepts a list of Withdraw → Call → Deposit actions and only checks balances at the end. Borrow N, do anything in callFunction, deposit N + 2 wei, done. The 2-wei dust is the entire fee.SoloMargin lives at
0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e on mainnet, and for years it was the cheapest source of WETH/DAI/USDC liquidity for both legitimate arbitrageurs and exploiters. The receiver must implement ICallee.callFunction(address sender, Account.Info memory accountInfo, bytes memory data).Uniswap V3 — flash() separated from swaps
Unlike V2, where flash logic was embedded in
swap, V3 exposes a dedicated flash() on every pool:1function flash(2 address recipient,3 uint256 amount0,4 uint256 amount1,5 bytes calldata data6) external override lock noDelegateCall {7 uint128 _liquidity = liquidity;8 require(_liquidity > 0, 'L');9 uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);10 uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);11 uint256 balance0Before = balance0();12 uint256 balance1Before = balance1();13 if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);14 if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);15 IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);16 require(balance0Before.add(fee0) <= balance0After, 'F0');17 require(balance1Before.add(fee1) <= balance1After, 'F1');18}
Fee equals the pool's swap fee tier (0.01%, 0.05%, 0.30%, or 1%). The borrower implements
uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) and must validate that msg.sender is a canonical V3 pool — CallbackValidation.verifyCallback(factory, decoded.poolKey). Forgetting this check turns the callback into a public free-money function. For deeper context on V3 mechanics, see our Uniswap V3 deep dive and Q64.96 fixed-point primer.Balancer V2 — zero fee, vault-wide liquidity
The Balancer Vault at
0xBA12222222228d8Ba445958a75a0704d566BF2C8 holds all token balances for every pool in a single contract, exposing them as a unified flash loan source with no fee.1// IVault2function flashLoan(3 IFlashLoanRecipient recipient,4 IERC20[] memory tokens,5 uint256[] memory amounts,6 bytes memory userData7) external;89// IFlashLoanRecipient10function receiveFlashLoan(11 IERC20[] memory tokens,12 uint256[] memory amounts,13 uint256[] memory feeAmounts, // currently always 014 bytes memory userData15) external;
This makes Balancer the modern attacker's preferred provider for any payload that doesn't need DAI specifically. The Penpie attacker borrowed wstETH, sUSDe, egETH, and rswETH from Balancer in a single call. For Balancer's underlying pool architecture, see our Balancer protocol architecture deep dive.
MakerDAO DssFlash — native DAI minting
Rather than holding DAI as inventory, the Maker Flash Mint Module mints DAI directly into the borrower's address up to a governance-configured
line (debt ceiling), then burns it via vat.heal at the end of the transaction. Fee (toll) was lowered to 0% in September 2021. The module conforms to ERC-3156:1function onFlashLoan(2 address initiator,3 address token,4 uint256 amount,5 uint256 fee,6 bytes calldata data7) external override returns (bytes32);
This is how attackers materialize hundreds of millions of DAI out of thin air. The Cream Finance attacker minted ~$500M DAI from Maker in a single call.
Side-by-side fee structure

- dYdX Solo Margin — 2 wei (effectively free)
- Balancer V2 — 0%
- MakerDAO DssFlash — 0% (DAI only)
- Uniswap V3 — pool fee tier (0.01% / 0.05% / 0.30% / 1%)
- Aave V3 — 0.05%
- Aave V2 — 0.09%
Most modern exploits stack providers: borrow whatever Balancer has, top up with Maker DAI, fall back to Aave for the long tail. The UwU Lend attacker pulled $3.796B of liquidity from Aave V3, Aave V2, Uniswap V3, Balancer, Maker, Spark, and Morpho in a single transaction.
The four-phase anatomy

Every flash loan attack — without exception across the case studies that follow — fits the same four-phase template.
Phase 1 — Borrow. The attacker contract calls a flash loan provider's entrypoint. Control returns to the attacker inside the callback, holding the borrowed assets. No collateral was posted; the only commitment is the implicit promise to repay before the function returns.
Phase 2 — Manipulation. Inside the callback, the attacker uses the borrowed capital to push the protocol into a state it doesn't normally see. This is where the bug lives. The capital skews an AMM reserve ratio (oracle manipulation), inflates a vault share price (donation attack), accumulates governance tokens to pass a malicious proposal (governance attack), reaches a precision-loss tick boundary (KyberSwap), triggers an arithmetic overflow (Cetus), or simply re-enters a function while reward accounting is mid-update (Penpie).
Phase 3 — Extraction. With the protocol's worldview corrupted, the attacker performs the action that monetizes the corruption: borrowing more than they should be allowed to, liquidating someone (often themselves, with a discount), redeeming inflated shares, executing a swap at the wrong price, or claiming inflated rewards.
Phase 4 — Repayment. The attacker reverses any temporary state (often by swapping back through the AMM they manipulated), repays principal plus fee, and walks away with the delta. The transaction settles atomically; the protocol's books reflect a real, irreversible loss.
A canonical attacker contract skeleton looks like this:
1contract Exploit is IFlashLoanReceiver {2 IPool constant POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);3 IVictimProtocol constant VICTIM = IVictimProtocol(0x...);45 function pwn(uint256 borrowAmount) external {6 address[] memory assets = new address[](1);7 assets[0] = USDC;8 uint256[] memory amounts = new uint256[](1);9 amounts[0] = borrowAmount;10 uint256[] memory modes = new uint256[](1); // 0 = repay11 // Phase 1: Borrow12 POOL.flashLoan(address(this), assets, amounts, modes,13 address(this), "", 0);14 // Phase 4 completes here when executeOperation returns15 IERC20(USDC).transfer(msg.sender, IERC20(USDC).balanceOf(address(this)));16 }1718 function executeOperation(19 address asset,20 uint256 amount,21 uint256 premium,22 address /*initiator*/,23 bytes calldata /*params*/24 ) external override returns (bool) {25 require(msg.sender == address(POOL), "auth");2627 // Phase 2: Manipulation28 IERC20(asset).approve(address(VICTIM_DEX), amount);29 VICTIM_DEX.swap(asset, otherToken, amount); // skew the oracle3031 // Phase 3: Extraction32 VICTIM.depositOrBorrowAtNowBrokenPrice(...);33 VICTIM.withdrawOrLiquidateForProfit(...);3435 // unwind manipulation so we can repay36 VICTIM_DEX.swap(otherToken, asset, ...);3738 // Phase 4: Repayment (Aave will pull amount + premium)39 IERC20(asset).approve(address(POOL), amount + premium);40 return true;41 }42}
Replace Phase 2 and Phase 3 with the specific bug, and you have every attack in this post.
Hunt these vulnerability classes

Flash loans don't break protocols. They exploit existing flaws under conditions you didn't test. Here is the catalog auditors should be running through on every code path that touches asset valuation, share accounting, voting, or rewards.
Single-source spot oracles. A protocol that prices a token by reading
getReserves() on one AMM pool, or pricePerShare() on one vault, is asking to lose all its money. Flash-loaning a 9-figure swap into a low-liquidity pool moves the spot price by 50%+ for the duration of the transaction. Every attack from bZx in 2020 through UwU Lend in 2024 is a variation on this theme. Curve has explicitly warned that get_p() returns an instantaneous spot price and must not be used as an oracle — a warning UwU Lend learned about the hard way. Our oracle manipulation deep dive and Curve finance core mechanics walk through the math.Donation attacks on empty markets. When a vault or lending market is initialized with zero or near-zero supply, a direct token transfer (ERC-20
transfer, not deposit) inflates the underlying balance without minting shares, sending the share price to absurd values. Subsequent depositors round down to zero shares. Compound V2 forks (Hundred, Onyx, Sonne, Starlay) and ERC-4626 vaults with insufficient virtual offset are the canonical victims. See our Yearn vault security write-up for the ERC-4626 hardening pattern.Governance attacks. If voting power is determined by a snapshot at the current block, a flash loan can produce a temporary supermajority. Beanstalk's
emergencyCommit accepted votes from tokens deposited in the same transaction the proposal was executed. 182M was gone. We dissect the broader pattern in DAO governance attacks.Reentrancy combined with flash-borrowed capital. Classic reentrancy is bad enough; pair it with a flash loan and the attacker can re-enter through any callback an external contract gives them — ERC-721
onERC721Received, ERC-777 hooks, custom token callbacks. Penpie's reentrancy through a malicious SY contract during reward accounting is the 2024 archetype. Read-only reentrancy — where a view function returns mid-update state — is a subtler variant.Tick-boundary precision exploits. Concentrated liquidity AMMs do tick math at high precision. KyberSwap Elastic's
computeSwapStep used two slightly different arithmetic paths for the same boundary check, allowing a swap to bypass _updateLiquidityAndCrossTick and double-count liquidity. Cetus's checked_shlw had the wrong overflow mask, allowing the attacker to mint massive liquidity for a single token unit.Liquidation-logic flaws. Functions that grant the liquidator a reward larger than the bad debt — Euler Finance's
donateToReserves had no health check, and the dynamic close factor scaled the discount with how underwater the position was, so an attacker could donate themselves into a 75%-discount liquidation against themselves.Reward distribution math that uses a balance-before/balance-after delta to compute rewards but doesn't validate that the balance change came from the intended source. Penpie's
batchHarvestMarketRewards was vulnerable to malicious tokens "depositing" themselves as rewards via reentrancy.Cross-protocol composability. When protocol A reads protocol B's state for pricing, flash loans against B can warp A. Cream Finance read
pricePerShare() from a Yearn vault that internally relied on a Curve pool — three legos deep, three failure modes.Case study: Cetus protocol — May 22, 2025 — $223M
Cetus is the largest concentrated-liquidity DEX on Sui. The exploit was a u256 left-shift overflow in a shared math library, triggered by a flash-swap-funded liquidity provision.
The buggy primitive was
checked_shlw, which was supposed to abort if shifting a u256 left by 64 bits would overflow. The implementation compared the input against 0xFFFFFFFFFFFFFFFF << 192 instead of 0x1 << 192 — the threshold was 2⁶⁴ times too lenient. Values that should have aborted slid through, and the actual shift truncated the high bits, producing a tiny intermediate.That intermediate fed
get_delta_a, the function that computes how much of token A is required for a liquidity amount. With the right liquidity input, the function returned 1. The attacker added "1 unit of token A" of deposit and was credited with 10,365,647,984,364,446,732,462,244,378,333,008 units of liquidity.The flow:
- Flash-swap 10,024,321 haSUI from a Cetus pool itself.
- Swap to push the pool's price into an extreme tick range (~
[300000, 300200]). - Open a position with
liquidity = L*(carefully chosen) and 1 unit of token A; the overflow truncates the required deposit to 1. - Remove liquidity; receive enormous reserves out.
- Repay the flash swap; profit.
The same vulnerable primitive lived in Kriya, Momentum, and Bluefin. Roughly 162M via emergency on-chain action. Cetus relaunched a week later with a Sui Foundation $30M USDC loan, treasury reserves, and recovered funds. Affected LPs received 85–99% of their original liquidity back.
The bug existed in early Aptos code, was flagged by OtterSec in 2023, and was supposedly fixed — but a regression reintroduced it on Sui, and a Zellic April-2025 audit didn't flag it because the math library appears to have been out of scope. This is the canonical "shared math library" failure mode: one helper function, four protocols, $223M. Our 2025 exploit lessons covers Cetus alongside the year's other infrastructure-class incidents.
Case study: Penpie (Pendle) — September 3, 2024 — $27M
Penpie is a Convex-style yield-boosting layer on top of Pendle Finance. The exploit chained three issues: permissionless market registration, missing reentrancy protection on
PendleStakingBaseUpg::batchHarvestMarketRewards, and a Balancer flash loan.Pendle's
PendleMarketFactoryV3.createNewMarket was permissionless, and PendleMarketRegisterHelper.registerPenpiePool was gated by a onlyVerifiedMarket modifier whose verification was "the market exists in the Pendle factory." That meant the attacker could register an attacker-controlled market on Penpie by first creating a Pendle market backed by an attacker-controlled SY (Standardized Yield) token.The flow:
- Attacker creates a fake market and registers it on Penpie. Configures two high-value reward tokens:
0x6010_PENDLE-LPTand0x038c_PENDLE-LPT. - Attacker deposits a small amount of fake market LP into Penpie.
- Attacker takes a flash loan from Balancer of wstETH, sUSDe, egETH, and rswETH.
- Calls
PendleStaking.batchHarvestMarketRewardson the fake market. batchHarvestmeasuresbalanceOf(rewardToken)before and afterredeemRewards(). DuringredeemRewards, the malicious SY contract reenters Penpie'sdepositMarket, depositing flash-loaned tokens (which are configured as the reward tokens). The balance delta is now interpreted as a "reward" attributable to the attacker.- Reentrancy ends; Penpie credits the attacker as the only depositor in the fake market with the inflated rewards.
- Attacker calls
multiclaim, withdraws, and repays the Balancer flash loan.
Loss: 11,113.6 ETH (~105M.
Notable detail: the original Penpie audit by Zokyo (2023) didn't flag reentrancy because pool registration was permissioned at the time. Permissionless registration was added in May 2024 but
PendleStakingBaseUpg was out of scope for the AstraSec audit of that change. Our post-audit security guide explains why every config change requires a re-scope.Case study: KyberSwap Elastic — November 22, 2023 — $48M
KyberSwap Elastic is a Uniswap V3-style concentrated liquidity AMM with an additional "reinvestment curve" that auto-compounds LP fees as additional in-range liquidity. The reinvestment curve was the source of the bug.
Inside
computeSwapStep, the function calcReachAmount computed the amount of input required to push the pool to the next tick boundary. The decision of whether the swap actually crossed the tick was made by comparing the resulting nextSqrtP against targetSqrtP. Because the reinvestment liquidity was treated differently between the two calculations, calcReachAmount could return a value that produced a nextSqrtP slightly greater than targetSqrtP for the maximum input — a 1-wei precision asymmetry. The attacker used a swap amount of amountToCrossTick - 1. The first check (input amount ≤ max) said "doesn't cross"; the actual price update produced a sqrtP past the boundary; _updateLiquidityAndCrossTick was therefore not called, and the liquidity at the crossed tick remained on the books.Attack flow on USDC/ETHx (mainnet, tx
0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475):- Flash-loan 500 ETHx from a Uniswap V3 pool.
- Swap 246.754 ETHx for 32,389.63 USDC, pushing the pool's tick to 305000 — into a "vacuum zone" with no existing liquidity.
- Mint a tight position [305000, 305408] with 16 USDC and 5.87e-3 ETHX (3,321,338,298,606,975 liquidity).
- Remove most of that liquidity. Precisely 54,880,483,538,064 of "glitch liquidity" remains.
- Swap 244.08 ETHX, pushing tick to 305408 (the upper boundary). This is the swap that should have called
_updateLiquidityAndCrossTickto remove the attacker's range from active liquidity but doesn't, due to the precision bug. - Reverse-direction swap: trade 493.638 ETHx for USDC. The pool now applies the un-removed liquidity plus the actual external liquidity, double-counting. The attacker receives an outsized USDC payout.
- Repay the flash loan; pocket 32,359 USDC + 2,548 ETHx per cycle.
Total losses across Ethereum (15.5M), Arbitrum (1.2M), Base (23K). The attacker (
0x502...) demanded full control of KyberDAO, executive equity, and ownership of the company in exchange for funds — a now-iconic on-chain ransom note.Case study: UwU Lend — June 10, 2024 — $23M
UwU Lend is a fork of Aave v2 deployed by 0xSifu with a custom fallback oracle for sUSDe. The oracle,
sUSDePriceProviderBUniCatch, took the median of 11 price sources for sUSDe. Five of those sources called get_p() on Curve pools — Curve's spot-price function, which the Curve team explicitly tells projects not to use as an oracle.The flow:
Get the DeFi Protocol Security Checklist
15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.
No spam. Unsubscribe anytime.
- Flash-borrow ~$3.796B in stablecoins and ETH from Aave V3, Aave V2, Uniswap V3, Balancer, MakerDAO, Spark, and Morpho.
- Swap a large portion into USDe and dump into a Curve USDe/sUSDe pool, depressing the sUSDe spot price reported by
get_p()by ~4%. - Borrow sUSDe from UwU Lend at the depressed price (sUSDe = $0.99) using other tokens as collateral.
- Reverse the Curve swap, pushing sUSDe up to $1.03.
- Existing leveraged positions (notably the attacker's earlier deposits) become liquidatable at the inflated price; the attacker liquidates them, receiving uWETH at the artificially high collateral valuation.
- Repay flash loans; net ~23M across three transactions.
Attacker EOA:
0x841dDf093f5188989fA1524e7B893de64B421f47. Attacker contract: 0x21C58d8F816578b1193AEf4683E8c64405A4312E. Sample tx: 0xca1bbf3b3202c89232006f1ec6624b56242850f07e0f1dadbe4f69ba0d6ac3.Curve founder Michael Egorov was personally affected, losing 23.5M CRV (~3.7M via residual misconfigurations. The PeckShield audit explicitly excluded oracles from scope; UwU's modifications to Aave's fallback oracle weren't independently re-audited.
Case study: Sonne Finance — May 14, 2024 — $20M
Sonne is an Optimism-deployed Compound V2 fork. The vulnerability is the well-known donation attack on a freshly deployed market, exploited at least four times before — Hundred Finance (2.1M), Starlay ($2.1M).
The exchange rate of a cToken is
(totalCash + totalBorrows - totalReserves) / totalSupply. When totalSupply is tiny (e.g., 2 wei) and an attacker directly transfers a large amount of the underlying to the contract — bypassing mint so no shares are issued — totalCash explodes while totalSupply doesn't. The exchange rate becomes astronomical, and 1 wei of soToken is now worth millions of underlying.Sonne knew this. Their internal procedure was to (1) deploy the new market with 0% collateral factor, (2) mint and burn some cTokens to set a non-zero floor, then (3) raise the collateral factor. They split the procedure into multiple multisig transactions on a 2-day timelock. On Optimism the multisig executor was permissionless. The attacker watched the timelock, executed steps 1 and 3 — bringing up the new soVELO market and setting collateral factor to 35% — without ever doing step 2.
Attack on soVELO (tx
0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0):- Flash-loan 35,469,150 VELO from VolatileV2 AMM.
- Mint 2 wei of soVELO (the absolute minimum).
- Donate 35,469,150 VELO directly to the soVELO contract via
transfer.totalCashrises;totalSupplyis still 2 wei. New exchange rate: 17,735,851,964,756,377,265,143,988,000,000,000,000,000. - Transfer the 2 wei soVELO to a fresh contract, borrow 265 WETH against it (the 2 wei is "worth" 35.4M VELO).
- Call
redeemUnderlying(35,471,603,929,512,754,530,287,976)— a precision-loss redemption against the inflated rate. - Repay the flash loan; profit ~$20M across soWETH, soUSDC, and soVELO markets.
Sonne paused all Optimism markets ~25 minutes after the first attack. Seal contributors saved an additional ~100 of VELO to the markets, breaking the empty-market condition. yAudit had explicitly flagged "Unclear protection against Hundred Finance attack vector" in their audit. The lesson: any new-market creation procedure that can be split or partially executed by an unrelated party defeats the procedural mitigation. Bundle everything into one transaction, or make execution permissioned.
Case study: Euler Finance — March 13, 2023 — $197M
Euler's exploit didn't manipulate any oracle. It used Euler's own functions in their intended order, with unintended consequences.
The bug:
donateToReserves(uint subAccountId, uint amount) was added in eIP-14 to fix a separate first-depositor exchange-rate bug. It allowed a user to donate eTokens (Euler's collateral receipt) to the protocol reserves. The function did not call checkLiquidity — donating did not enforce the post-condition that the donating account remained healthy.1/// @notice Donate eTokens to the reserves2/// @param subAccountId 0 for primary, 1-255 for a sub-account3/// @param amount In internal book-keeping units (as returned from balanceOf).4function donateToReserves(uint subAccountId, uint amount) external {5 // ... burn eTokens, increase reserves ...6 // BUG: no checkLiquidity()7}
Combined with Euler's recursive minting — deposit → eToken → borrow against eToken → re-deposit → eToken — this allowed an attacker to:
- Aave-flash-loan 30M DAI.
- Deposit 20M DAI into Euler, receive ~19.6M eDAI.
mint-recursively to leverage: 195.6M eDAI of collateral against 200M dDAI of debt.- Repay 10M of the dDAI to be healthy momentarily.
- Recurse again to even larger amounts.
- Call
donateToReserves(0, 100M eDAI). This burns 100M eDAI of attacker's collateral but does not burn the corresponding dDAI. Health drops massively below 1 with no liquidity check at this entry point. - From a different attacker contract (the "liquidator"), call
liquidate()against the now-underwater first contract. Euler's dynamic close factor gives the liquidator a discount that scales with how underwater the position is — at maximum, 20% discount on up to 75% of the remaining collateral. Liquidator receives 310M eDAI and 259M dDAI. - Withdraw 38.9M DAI from Euler.
- Repay flash loan; net ~$8.87M per pool.
Repeated across DAI, USDC, stETH, and wBTC for a total ~$197M. Attacker EOA
0xb66cd966670d962c227b3eaba30a872dbfb995db; exploit contract 0x036cec1a199234fc02f72d29e596a09440825f1c.Six audits had reviewed Euler's code. Sherlock had insured the audit and paid out a 240M (more than was stolen, due to ETH price appreciation during negotiations). The Cyfrin and BlockSec post-mortems both stress that this attack would have been caught by invariant tests asserting "no protocol action should leave an account where risk-adjusted liability > risk-adjusted assets." Our fuzzing and formal verification write-up covers the tooling.
Case study: Beanstalk — April 17, 2022 — $182M
Beanstalk's
emergencyCommit allowed any proposal that had been on-chain for 24 hours and held a 2/3 supermajority of Stalk (governance) tokens to be executed in a single transaction. Voting power was determined by current deposits in Beanstalk's Diamond contract — including deposits made in the same transaction as the vote.The attacker submitted two malicious BIPs (proposals) on April 16 and waited 24 hours. At 12:24 UTC on April 17:
- Aave / Uniswap / SushiSwap flash loans totaling >$1B (DAI, USDC, USDT, plus BEAN and LUSD from DEXes).
- Convert to Curve LP tokens that count as Beanstalk governance weight when deposited.
- Deposit into Beanstalk; instantly hold 79% of voting Stalk.
- Call
emergencyCommiton the malicious BIP, which transfers protocol reserves to attacker (and 250K USDC to the Ukraine donation address — performative cover). - Withdraw, swap back, repay flash loans.
Net: 182M protocol losses. Omniscia's post-mortem identified the root cause clearly: the governance functions were not in scope of any pre-launch audit, and the DAO design fundamentally trusted the current Stalk distribution rather than a snapshot taken at proposal creation.
Case study: Cream Finance — October 27, 2021 — $130M
Cream listed a Yearn vault token (
yUSDVault) as collateral. The PriceOracleProxy priced yUSDVault by reading vault.pricePerShare() directly. By manipulating the underlying Curve y-pool and donating Yearn 4-Curve tokens directly to the vault, the attacker could double the vault's reported price per share without any "real" deposit.Two coordinated contracts A and B (initiated by
0x24354d31bc9d90f6254709c32049cf866b):- MakerDAO flash-mint $500M DAI (DssFlash).
- Deposit DAI into Curve y-pool, receive yDAI; convert to yUSD.
- Aave-flash-borrow $2B in ETH; deposit ETH on Cream as collateral in contract B.
- Borrow yUSD from Cream against the ETH; transfer yUSD to contract A and deposit. Repeat the loop, accumulating ~3B in dDebt in contract B.
- Withdraw $500M of yUSDVault from the vault, draining the vault's underlying.
- Donate $10M of Yearn 4-Curve directly to the vault, doubling
pricePerShare. - Cream's oracle now values contract A's 3B. Contract A is massively over-collateralized in Cream's eyes; contract A borrows back the 130M of Cream's other markets.
- Repay all flash loans.
Net: ~$130M, all of Cream Finance's available liquidity at the time. This was Cream's third major exploit; the protocol never recovered.
Case study: Harvest Finance — October 26, 2020 — $24M
The textbook-class economic exploit. Harvest's
fUSDC and fUSDT vaults priced their shares using underlyingBalanceWithInvestment(), which read the live invested balance from the Curve y-pool. Curve's y-pool exchange rates moved with reserve ratios.The cycle (tx
0x35f8d2f572fceaac9288e5d462117850ef2694786992a8c3f6d02612277b0877, run 30+ times):- Uniswap flash-loan ~18M USDT.
- Swap 17.2M USDT → USDC in Curve y-pool. USDC's pool weight rises; USDT's drops. The y-pool now overvalues USDC.
- Deposit ~50M USDC into fUSDC vault. Because the vault values USDC using the now-skewed y-pool reserves, share price drops to ~0.97; attacker receives more fUSDC than fair.
- Reverse swap on Curve: 17.2M USDC → USDT, restoring pool ratios. fUSDC share price rises back to ~0.98.
- Withdraw fUSDC for ~50.6M USDC. Net: ~$619K profit per iteration.
- Repay flash loan.
30 iterations against fUSDC in 4 minutes; 13 iterations against fUSDT in another 3 minutes. Total profit ~33M because remaining vault depositors held now-devalued shares. Harvest's "arbitrage check" had a 3% threshold — the per-iteration price impact was ~1%, just under the limit. Audited by multiple firms pre-launch.
Case study: bZx — February 14 and 18, 2020 — 645K
The originals.
Attack 1 (Feb 14):
- dYdX flash-loan 10,000 ETH.
- Use 5,500 ETH on Compound to borrow 112 wBTC.
- Through bZx's
mintWithEther, open a 5x short ETH/wBTC position with 1,300 ETH. bZx's logic dispatches the swap through Kyber, which routes to Uniswap. The huge market order on the small Uniswap wBTC pool drives the price 3x. The bug: bZx's slippage check failed to fire. - Sell the 112 wBTC on Uniswap into the inflated price — pocket ~6,871 ETH.
- Repay dYdX 10,000 ETH; walk away with
1,193 ETH ($370K).
Attack 2 (Feb 18): Pure oracle attack. Flash-loan ETH, swap large amount into sUSD via Kyber's Synthetix reserve, push sUSD's spot price up, then borrow ETH from bZx using sUSD as collateral at the inflated price. Walk with 2,378 ETH ($645K).
These were the proof-of-concept that flash loans could weaponize any economic miscalculation. Five years later, half the protocols on the rekt leaderboard are losing money to a flash-loan amplification of the same bug class.
Defenses that actually hold

The attacks above fail when any one of the following is true. Every code path that touches asset valuation, share accounting, voting, rewards, or new-market initialization is a flash-loan attack surface, and protocols need multiple of these defenses, not one.
Use TWAP or decentralized oracles, not spot prices
Uniswap V3 exposes
observe() for cumulative tick observations — sample over 30+ minutes for a manipulation-resistant TWAP. Better still, push-based decentralized oracles (Chainlink, Pyth) are sourced off-chain from many exchanges and aggregated by many node operators; a flash-loaned swap on one DEX doesn't move them. The attack surface in practice is wherever auditors find a custom oracle, "fallback" oracle (UwU Lend), or a price derived from vault.pricePerShare() against a vault that itself reads an AMM (Cream).Aggregate sources, but make sure they're independent
UwU Lend used a median of 11 prices, but 5 came from Curve
get_p() — a single attack vector spanning 5 of 11 inputs. Audit oracle sources for correlated manipulation paths.Block-delay state-sensitive operations
A user who mints shares this block should not be able to redeem until next block. A voter who deposits this block should not have voting power until snapshot N blocks later. This single defense breaks every flash-loan governance attack. Compound's Governor Bravo uses
getPriorVotes(account, blockNumber) against snapshots taken before proposal creation. OpenZeppelin's Governor framework defaults to this pattern. Beanstalk's mistake was reading current voting power.Mint-and-burn dead shares atomically on initialization
For Compound V2 forks: when adding a market, in the same transaction, mint a non-trivial amount of cToken (e.g., 1e8 wei) and send to
address(0). This makes totalSupply unbreachably non-zero and the donation attack uneconomical.For ERC-4626, use OpenZeppelin's
_decimalsOffset() to add virtual shares and assets:1function _convertToShares(uint256 assets, Math.Rounding rounding)2 internal view virtual override returns (uint256)3{4 return assets.mulDiv(5 totalSupply() + 10 ** _decimalsOffset(),6 totalAssets() + 1,7 rounding8 );9}
A
_decimalsOffset of 6 means the attacker would need to lose ~10⁶ × the victim deposit to extract the victim's deposit — economically irrational.Reentrancy guards everywhere, including read-only
OpenZeppelin's
ReentrancyGuard blocks classic reentrancy, but a function that reads from a contract whose state is mid-update during a reentrant call (read-only reentrancy) needs a different defense: either expose a notReentrant view modifier or use a lock() pattern that any read also calls. Penpie's redeemRewards had nonReentrant, but batchHarvestMarketRewards did not — and the malicious SY contract reentered through batchHarvestMarketRewards, not redeemRewards. Audit every public function that either calls or is called by a function with nonReentrant. If it isn't itself guarded, ask why.Checks-Effects-Interactions, mechanically
Any function that updates a balance, share, or reward and then makes an external call must update state first. Penpie's
batchHarvestMarketRewards violated this by computing rewards as balanceAfter - balanceBefore across an external call.Slippage limits on every AMM-touching operation
amountOutMinimum on swaps, minSharesOut on deposits, minAssetsOut on redemptions. Harvest Finance's vault accepted whatever share count its broken pricing produced; a strict slippage gate on deposit() would have rejected the attacker's transactions.Per-block borrow limits and global rate limits
Aave V3 uses
borrowCap and supplyCap. For sensitive markets, set them low enough that a flash loan can't fully drain the position. Not a perfect defense — caps damage rather than preventing the bug — but a recoverable failure mode.Invariant testing at the protocol level
This is the defense that would have caught Euler. Foundry's
invariant_* tests, Echidna, Halmos, and Certora can express "no sequence of public calls leaves any user account with liability > collateral" or "totalAssets() never decreases except via withdraw()". Cetus's bug was caught in a 2023 audit on the Aptos variant — fixed there, regressed on Sui — and would have been caught by a property test like "for all liquidity L and tick range [a,b], get_delta_a(L,a,b) does not return 1 unless L is small." Our fuzz testing primer walks through writing these.Audit code paths, not just the audit-scope diff
Most of the post-mortems above name specific functions auditors looked at — and the bug was in a function they didn't look at, or one whose call graph crossed the audit boundary. UwU Lend's PeckShield audit excluded oracles. Astrasec's Penpie audit excluded
PendleStakingBaseUpg because it was unchanged. Cetus's Zellic audit excluded the math library. Audit firms should produce and publish their scope; protocol teams should ensure that any code on the call path of a function in scope is itself in scope. See our pre-audit checklist for the scope-definition pattern that prevents this.Bundle multi-step admin procedures into a single transaction
When the in-between states are dangerous, bundle them. Sonne's "deploy market → seed → set collateral factor" was three transactions. The attacker only needed steps 1 and 3 — without step 2, the donation attack worked. A
Multicall or bundled multisig payload eliminates this race. Alternatively, restrict the executor role on timelock so only the team can submit the actual execution.Pause mechanisms with sub-30-minute response
Pendle saved ~162M of $223M. Pause is not a defense against a complete exploit, but it bounds tail risk against repeated cycles. Keep the pause role on a dedicated multisig with no other privileges, and rehearse the ceremony.
Monitor on-chain in real time
Forta, Hypernative, Cyvers, BlockSec Phalcon, and OpenZeppelin Defender provide real-time anomaly detection with alerts for governance proposals, large flash loans, oracle deviations, and
transferOwnership calls. UwU Lend was first detected by Cyvers when ~20M+ peak. The protocols that survive flash-loan attacks are the ones that respond in minutes.The tx.origin != msg.sender anti-pattern
A surprising number of contracts try to detect "is this call coming from a flash loan?" by checking
require(tx.origin == msg.sender) — i.e., requiring an EOA caller. This is not a flash loan defense.- EIP-3074 and various account abstraction patterns blur
tx.origin. - An attacker can call the protected function from a non-flash-loan contract; the check stops all contracts (including legitimate integrators) but doesn't stop a determined attacker who flash-loans, deposits funds into an EOA-like proxy, and calls from a fresh transaction.
- It doesn't help against attacks that don't need to be in the same transaction — multi-block governance with snapshot voting at the current block, for example.
The honest defense is to make the operation safe under the assumption that the caller has unlimited capital. That is the threat model. Anything else is wishful thinking.
What this means for auditors
When you open a new audit engagement, before reading any code:
- List every external view function that returns a price, share count, or vote weight. These are your flash-loan attack surfaces.
- List every public state-mutating function, and for each, identify what state it reads to validate inputs. Any state read from another contract — or from this contract, before all updates have committed — is a potential corruption point.
- List every function that callbacks into the caller (ERC-721
onERC721Received, ERC-777 hooks, custom callbacks, externally-controlled token transfers). Each is a potential reentrancy entry. - List every newly-deployed-pool / newly-listed-asset code path. Run the donation attack mentally before you look at the test suite.
- Demand a clear call-graph reason for any out-of-scope function reachable from in-scope code.
The pattern repeats. Five years after bZx, half the protocols on the rekt leaderboard are losing money to a flash-loan amplification of the same six bug classes — spot oracles, donation attacks, governance snapshots, reentrancy, precision loss, unchecked invariants. The signature is in the call graph: borrow → manipulate → extract → repay. If you can find a path through the protocol that completes that loop with a positive delta, an attacker will find it too — and they have a $223M reason to keep looking.
Get in touch
Zealynx Security audits smart contracts and protocol architectures with a focus on economic exploit classes — flash loan amplification, oracle manipulation, governance attacks, and invariant failures. If you are shipping a lending market, a concentrated-liquidity DEX, an ERC-4626 vault, or anything that prices an asset on-chain, a scope review takes one call.
Request a scope review — or browse our audit ROI breakdown and 2026 audit pricing guide before you book.
FAQ: flash loan attacks
1. What is a flash loan and how is it different from a regular loan?
A flash loan is an uncollateralized loan that is borrowed and repaid in the same atomic blockchain transaction. The lender does not check credit or hold collateral — instead, the EVM reverts the entire transaction if repayment fails, so the loan either succeeds in full or never happened at all. A regular crypto loan, by contrast, requires the borrower to lock collateral (e.g., 150% of the borrowed amount) for as long as the loan remains open. The key consequence: a flash loan lets anyone command nine-figure capital for the duration of one transaction without owning a single token. See our flash loan glossary entry for the full mechanics.
2. Why do flash loans enable nine-figure exploits if the bug is the actual problem?
Most economic bugs in DeFi protocols — a 0.5% pricing error, a 1-wei tick precision asymmetry, an inflatable share price — only pay out at the scale of the attacker's capital. Without a flash loan, attacking a 370K loss into Cetus's $223M loss is not a different bug class — it is the same bug class plus larger amplification.
3. Does requiring a "no contracts" check (`tx.origin == msg.sender`) prevent flash loan attacks?
No. The
tx.origin == msg.sender check requires the caller to be an externally-owned account (EOA) rather than a contract. It blocks the most naive form of a flash loan exploit but it is not a real defense: account abstraction (EIP-4337) and EIP-3074 patterns blur the EOA distinction, and many flash-loan-amplified attacks (notably governance attacks like Beanstalk) span multiple transactions where the check never fires. It also breaks legitimate integrators (smart wallets, multisigs, automated keepers). The correct threat model assumes the attacker has unlimited capital within a transaction; design protocol invariants to hold under that assumption.4. What is a donation attack, and why is it specifically a flash loan vulnerability?
A donation attack (also called share inflation or first-depositor attack) is an exploit on vaults or lending markets whose share price is
totalAssets / totalSupply. When totalSupply is tiny — for example, 2 wei right after market creation — an attacker transfers a large amount of underlying asset directly to the contract (using ERC-20 transfer, bypassing deposit()). This raises totalAssets without minting shares, sending the share price to absurd values; subsequent depositors round down to zero shares, and the attacker withdraws both their tiny position and the victim's deposit. Flash loans turn the attack from "needs 35M of VELO from any AMM, attack, repay" — costing only gas. Sonne Finance lost $20M to exactly this pattern in 2024.5. What is a TWAP oracle and why does it stop most flash loan price manipulation?
A time-weighted average price (TWAP) oracle reports the average price of an asset over a window of time (e.g., 30 minutes) rather than the instantaneous spot price at the current block. Because a flash loan is bounded by a single transaction, an attacker can only push the spot price for that single block — the average over 30 minutes barely moves. Uniswap V3 exposes
observe() for this purpose, and decentralized push oracles (Chainlink, Pyth) achieve the same protection by aggregating prices off-chain across many exchanges. The remaining attack surfaces are short TWAP windows (under 5 minutes can still be cost-effective to manipulate over multiple blocks), thinly-traded assets, and protocols that price one asset by reading another contract's spot price — see our oracle manipulation guide for the full taxonomy.6. We have an audit. Are we safe from flash loan attacks?
Not necessarily. Six firms audited Euler before its $197M exploit; the bug was in a function (
donateToReserves) that did exactly what the spec said but missed a post-condition (a checkLiquidity call). PeckShield audited UwU Lend but excluded the oracle; AstraSec audited Penpie's permissionless registration change but excluded PendleStakingBaseUpg; Zellic audited Cetus on Aptos two years before the Sui bug. An audit confirms that the in-scope code passes a specific set of expert-applied checks within a fixed timeframe. The defenses that hold are layered: TWAP oracles + reentrancy guards on every public function + invariant tests + bundled deployments + monitoring + pause mechanisms. Our post-audit security playbook covers what to do after the report lands.Glossary
| Term | Definition |
|---|---|
| Flash loan | Uncollateralized loan borrowed and repaid in a single atomic transaction; secured by EVM revert rather than collateral. |
| Donation attack | Empty-vault share-price inflation via direct token transfer that rounds subsequent depositors down to zero shares. |
| Price oracle manipulation | Skewing a protocol's price feed (typically an AMM spot price) to trigger mispriced borrows, liquidations, or redemptions. |
| TWAP oracle | Time-weighted price feed that aggregates over multiple blocks, neutralizing single-transaction manipulation. |
| Reentrancy attack | An external call returning control to an attacker before state updates commit, allowing recursive misuse. |
| Read-only reentrancy | A view function returning mid-update state during a reentrant call, corrupting downstream consumers. |
| Governance attack | Exploit of a protocol's voting mechanics — often via flash-loaned voting tokens — to pass malicious proposals. |
| Slippage | Difference between expected and executed swap price; the gate that should reject manipulated AMM trades. |
| Invariant testing | Property-based fuzzing that asserts protocol-wide truths (e.g., "no account ends with liability > collateral"). |
Get the DeFi Protocol Security Checklist
15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.
No spam. Unsubscribe anytime.


