Flash loans are one of DeFi’s most elegant primitives and, when aimed at a vulnerable protocol, one of its most devastating weapons. In a single atomic transaction, an attacker can borrow hundreds of millions of dollars, reshape market state, drain a treasury, and repay every cent — all before the next block is mined. Understanding how this is possible requires understanding both the mechanics of the loan itself and the assumptions baked into the protocols it targets.
1. How Flash Loans Work Mechanically
A flash loan is an uncollateralized loan that must be borrowed and repaid within a single, atomic blockchain transaction. If the loan is not fully repaid by the end of that transaction, the entire operation reverts as if it never happened. This atomicity guarantee is the defining property: borrow and repay in one transaction, or the chain rolls back every state change.
Flash loans were designed for entirely legitimate use cases: arbitrage, collateral swaps, and debt refinancing. The attack surface they open is not a flaw in the flash loan mechanism itself — it is a consequence of what becomes possible when you can briefly command unlimited capital inside a single call frame.
1.1 Aave V3: flashLoan and executeOperation
Aave is the canonical flash loan provider. Aave pioneered flash loans in 2020 with a 0.09% fee and supports an extensive selection of assets including ETH, USDC, DAI, and WBTC.
The Aave V3 flow works as follows:
- Your contract calls
pool.flashLoan(receiverAddress, assets[], amounts[], ...). - The Aave pool transfers the requested tokens to
receiverAddress. - The pool immediately calls
receiverAddress.executeOperation(assets, amounts, premiums, initiator, params). - Inside
executeOperation, your contract executes arbitrary logic. - Before
executeOperationcompletes, your contract must approve the Aave Pool’s aToken contract to spend at leastamount + totalPremiumof the asset. Without this approval, the Aave protocol cannot pull the funds back, and the entire transaction reverts.
The IFlashLoanSimpleReceiver interface from Aave’s deployed contracts makes the signature explicit:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IFlashLoanSimpleReceiver {
/**
* @notice Called by Aave after sending flash-borrowed tokens to this contract.
* @param asset The ERC-20 token borrowed.
* @param amount Amount borrowed.
* @param premium Fee owed on top of amount.
* @param initiator Address that called pool.flashLoanSimple().
* @param params Arbitrary encoded data passed by the initiator.
* @return True if execution succeeded and repayment approval is set.
*/
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool);
}
A minimal honest receiver:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract HonestReceiver is IFlashLoanSimpleReceiver {
IPool public immutable POOL;
constructor(address pool) {
POOL = IPool(pool);
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata /* params */
) external override returns (bool) {
// --- do useful work here (arbitrage, collateral swap, etc.) ---
// Repayment: approve pool to pull back amount + fee
uint256 totalDebt = amount + premium;
IERC20(asset).approve(address(POOL), totalDebt);
return true;
}
function requestLoan(address asset, uint256 amount) external {
POOL.flashLoanSimple(
address(this), // receiver
asset,
amount,
"", // params
0 // referral code
);
}
// Required by interface
function ADDRESSES_PROVIDER() external view override returns (address) {
return POOL.ADDRESSES_PROVIDER();
}
function POOL() external view override returns (IPool) {
return POOL;
}
}
The FlashLoanLogic contract calls the executeOperation function on your receiverAddress. Inside that callback you can implement any desired logic: arbitrage between DEXes, liquidating positions, swapping collateral, or any other operation completable within a single transaction.
1.2 Uniswap V3: Flash Swaps via uniswapV3FlashCallback
Uniswap V3 offers a different flavour called a flash swap. A flash loan is initiated from a lending protocol like Aave, where you borrow tokens from a lending pool and must return the same tokens plus a fee. A flash swap is initiated from a DEX like Uniswap, where you withdraw tokens from a trading pair.
In Uniswap V3 you call IUniswapV3Pool.flash(recipient, amount0, amount1, data). The pool sends you amount0 of token0 and amount1 of token1, then calls recipient.uniswapV3FlashCallback(fee0, fee1, data). Uniswap V3 flash loans enable seamless DEX arbitrage with concentrated liquidity pools.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { IUniswapV3FlashCallback } from
"@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol";
import { IUniswapV3Pool } from
"@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
contract UniFlashReceiver is IUniswapV3FlashCallback {
IUniswapV3Pool public immutable POOL;
constructor(address pool) {
POOL = IUniswapV3Pool(pool);
}
/// @notice Initiates the flash. amount0 = WETH, amount1 = USDC.
function initFlash(uint256 wethAmount, uint256 usdcAmount) external {
bytes memory data = abi.encode(msg.sender);
POOL.flash(address(this), wethAmount, usdcAmount, data);
}
/// @notice Called by the pool after sending tokens.
function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external override {
require(msg.sender == address(POOL), "untrusted pool");
address initiator = abi.decode(data, (address));
// --- Custom logic executes here with borrowed tokens ---
// Repay: return principal + fee for each token borrowed
address token0 = POOL.token0();
address token1 = POOL.token1();
IERC20(token0).transfer(address(POOL), POOL.balance0() + fee0);
IERC20(token1).transfer(address(POOL), POOL.balance1() + fee1);
}
}
The key structural difference: Uniswap V3 uses a push-repay model (you call transfer back into the pool) rather than Aave’s pull-repay model (the pool pulls via transferFrom). Both share the same invariant: if repayment fails, the whole transaction reverts.
2. The Conditions That Make a Protocol Vulnerable
The fundamental vulnerability is not the flash loan mechanism itself — it is protocols that rely on easily manipulable data sources. More precisely, a protocol is vulnerable when it treats within-transaction state as authoritative for decisions that have lasting economic consequences.
The strongest question to ask is: what values does your protocol treat as authoritative? If the answer includes single-pool spot prices, instantly realizable share values, or any state variable that can be moved dramatically by temporary capital, you have the basic ingredients for a flash-loan-enabled exploit.
The necessary conditions cluster into four categories:
| Condition | Description | Example |
|---|---|---|
| Spot-price oracle | Protocol reads reserve0/reserve1 as price | BZx (2020), Cheese Bank |
| Same-block state mutability | Governance weight or collateral value readable and usable in same tx | Beanstalk (2022) |
| Missing invariant enforcement | Reserve paths exist that skip health checks | Euler Finance (2023) |
| Reentrancy in value-changing callbacks | External call before state update | Compound forks (many) |
The prevention of oracle manipulation attacks proves particularly challenging because these attacks occur within a single transaction, inherently leaving little room for mitigation.
3. Price Oracle Manipulation
3.1 The Spot Price Trap
The most prevalent type of flash loan attack involves price oracle manipulation. Many DeFi protocols rely on decentralized exchanges to determine asset prices.
AMM spot prices reflect only the last trade in an isolated pool (governed by the x × y = k invariant), not global market value. With sufficient capital provided by flash loans, these prices are easily manipulated.
Protocols using a liquidity pool as their oracle are essentially 99.9% likely to be exploited because of the volatility in prices when leveraging flash loans.
3.2 Attack Transaction Sequence (Pseudocode)
TRANSACTION BEGIN (atomic)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. flash_borrow(AAVE, 10_000_000 USDC)
└── receive 10,000,000 USDC
2. swap(DEX_POOL, 10_000_000 USDC → TOKEN_X)
└── pool.reserve_USDC ↑ dramatically
└── pool.reserve_TOKEN_X ↓ dramatically
└── spot_price(TOKEN_X) = reserve_USDC / reserve_TOKEN_X → 4× inflated
3. victim_protocol.read_price(TOKEN_X)
└── returns manipulated spot price (4× real value)
4. victim_protocol.borrow(
collateral = TOKEN_X (valued at 4× real),
borrow_amount = 3× actual collateral value
)
└── victim hands over 3,000,000 USDC worth of assets
against collateral worth only ~750,000 USDC
5. swap(DEX_POOL, TOKEN_X → USDC)
└── price normalises back to fair market
6. repay(AAVE, 10_000_000 USDC + 9_000 fee)
PROFIT: ~2,241,000 USDC
TRANSACTION END
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.3 The Vulnerable Lending Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IUniswapV2Pair {
function getReserves()
external
view
returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}
/// @title VulnerableLender
/// @notice Uses Uniswap V2 spot price as its sole oracle — DO NOT DEPLOY
contract VulnerableLender {
IUniswapV2Pair public immutable oracle; // TOKEN_X / USDC pair
IERC20 public immutable usdc;
IERC20 public immutable tokenX;
mapping(address => uint256) public collateral; // TOKEN_X deposited
mapping(address => uint256) public debt; // USDC borrowed
constructor(address _oracle, address _usdc, address _tokenX) {
oracle = IUniswapV2Pair(_oracle);
usdc = IERC20(_usdc);
tokenX = IERC20(_tokenX);
}
function depositCollateral(uint256 amount) external {
tokenX.transferFrom(msg.sender, address(this), amount);
collateral[msg.sender] += amount;
}
function borrow(uint256 usdcAmount) external {
// ❌ VULNERABLE: reads AMM spot price — manipulable in one tx
(uint112 reserveX, uint112 reserveUsdc, ) = oracle.getReserves();
uint256 spotPrice = (uint256(reserveUsdc) * 1e18) / uint256(reserveX);
uint256 collateralValue = (collateral[msg.sender] * spotPrice) / 1e18;
require(collateralValue >= usdcAmount * 150 / 100, "undercollateralised");
debt[msg.sender] += usdcAmount;
usdc.transfer(msg.sender, usdcAmount);
}
}
3.4 Historical Examples
The core vulnerability in the first BZx attack was the protocol’s reliance on a single oracle for price determination, which allowed the attacker to manipulate the collateral pool using a flash loan. In the second BZx attack, the vulnerability was BZx’s usage of the Uniswap spot price as an oracle, which was manipulated to inflate the value of the collateral.
In the first BZx attack, the hacker borrowed $10 million in ETH through a flash loan and used it to manipulate the collateral pool by taking a 5× short position on the ETH/wBTC trading pair, causing significant slippage. In the second attack, the attacker used a flash loan to inflate the Uniswap Synthetix USD price to $2, then deposited sUSD into BZx as collateral to borrow more ETH than they should have been allowed.
In 2024 alone, price manipulation attacks accounted for over $52 million in losses across 37 incidents, making them the second most damaging attack vector.
4. Governance Attacks via Flash-Borrowed Voting Power
Attackers can use flash loans to temporarily acquire massive voting power in governance systems. They then vote to approve malicious proposals that transfer funds to their wallets, repay the loan, and disappear with the stolen assets.
4.1 The Beanstalk Attack
The Beanstalk Farms governance attack demonstrated a new dimension of flash loan risk. An attacker flash-borrowed over $1 billion in stablecoins from Aave, Uniswap, and SushiSwap, used the temporary voting power to pass malicious governance proposals, and drained the protocol of approximately $182 million. The attacker personally profited around $76–80 million after repaying the loans.
The vulnerability was precise: snapshot voting in Beanstalk granted immediate governance power to newly acquired tokens, without requiring any holding period.
According to Omniscia, Beanstalk’s governance module contained an emergencyCommit function that allowed the attackers to circumvent the usual lifecycle of a proposal and execute BIP-18 as soon as they attained super-majority voting power.
The attacker submitted a malicious Bean Improvement Proposal (BIP-18), referencing a precomputed smart contract address created using Ethereum’s CREATE2 opcode. Since the contract was not deployed at the time of proposal submission, its bytecode remained inaccessible on-chain, preventing community review.
4.2 Governance Attack Sequence (Pseudocode)
DAY 0:
attacker.deploy_malicious_bip_contract_via_CREATE2()
attacker.submit_proposal(bip_id=18, code=malicious_address)
// Contract not yet deployed — bytecode hidden from reviewers
DAY 1 (emergency commit window opens):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TRANSACTION BEGIN (atomic)
1. flash_borrow(AAVE, 500_000_000 DAI)
flash_borrow(UNISWAP, 32_000_000 BEAN)
flash_borrow(SUSHISWAP, stablecoins …)
2. add_liquidity(bean_3crv_pool, all_stablecoins)
└── receive ~79% of Beanstalk Silo LP tokens
3. deposit_into_silo(lp_tokens)
└── protocol mints Stalk (governance tokens)
└── attacker now holds ~70% of total Stalk supply
4. governance.emergencyCommit(bip_id=18)
└── 67% threshold satisfied → proposal executes immediately
└── BIP-18: transfer all Silo reserves to attacker wallet
└── BIP-19: send $250k BEAN to Ukraine charity (misdirection)
5. remove_liquidity(silo_positions)
└── receive all Beanstalk protocol funds
6. repay all flash loans + fees
PROFIT: ~$76,000,000 net
TRANSACTION END
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Being able to vote on and execute a proposal in the same transaction is a functionality that leaves a DAO vulnerable to a governance attack, due to the nature of flash loans.
5. Collateral Inflation Attacks
Collateral inflation attacks exploit lending protocols that calculate a user’s collateral value dynamically at borrow time. The attacker temporarily inflates that value — either by price manipulation or by exploiting accounting bugs — to borrow far beyond what legitimate collateral would permit.
5.1 The Euler Finance Attack
The hacker took a flash loan of about $30 million in DAI from Aave. They deposited $20 million of the DAI into Euler’s platform and received a similar amount in eDAI tokens. By leveraging the platform’s borrowing capabilities, the hacker borrowed ten times the original deposited amount. They used the remaining $10 million in DAI to repay part of the acquired debt and continued borrowing until the flash loan was closed. As a result, Euler lost around $197 million worth of cryptocurrency across DAI, wBTC, stETH, and USDC.
Every path that changes reserves, collateral, debt, or claimable balances must preserve the protocol’s core solvency invariants. Euler’s lesson is that an unusual helper path — such as a reserve donation — can be just as dangerous as the main borrow or transfer path if it bypasses the checks those main paths enforce.
5.2 Collateral Inflation in Solidity (Simplified)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title VulnerableVault — illustrates share-price inflation attack
/// @notice Based on ERC-4626 donation vulnerability pattern — DO NOT DEPLOY
contract VulnerableVault {
IERC20 public immutable asset;
uint256 public totalShares;
mapping(address => uint256) public shares;
constructor(address _asset) {
asset = IERC20(_asset);
}
/// @dev Share price = totalAssets() / totalShares
function totalAssets() public view returns (uint256) {
// ❌ VULNERABLE: reads raw token balance
// An attacker can donate tokens to inflate this without minting shares
return asset.balanceOf(address(this));
}
function deposit(uint256 assets) external returns (uint256 sharesOut) {
// ❌ First depositor + donation attack: if totalShares == 0 after
// a large donation, new depositors get almost zero shares
sharesOut = totalShares == 0
? assets
: (assets * totalShares) / totalAssets();
shares[msg.sender] += sharesOut;
totalShares += sharesOut;
asset.transferFrom(msg.sender, address(this), assets);
}
function withdraw(uint256 sharesIn) external {
uint256 assetsOut = (sharesIn * totalAssets()) / totalShares;
shares[msg.sender] -= sharesIn;
totalShares -= sharesIn;
asset.transfer(msg.sender, assetsOut);
}
}
Attack pattern against this vault:
TRANSACTION BEGIN (atomic)
1. flash_borrow(AAVE, 1_000_000 USDC)
2. vault.deposit(1 USDC)
└── totalShares = 1, totalAssets = 1
3. usdc.transfer(vault, 999_999 USDC) // direct donation — no shares minted
└── totalAssets = 1_000_000, totalShares = 1
└── share price = 1,000,000 USDC per share
4. victim deposits 500_000 USDC
└── sharesOut = (500_000 * 1) / 1_000_000 = 0 shares (integer truncation)
└── victim's 500,000 USDC is absorbed by vault with no shares issued
5. vault.withdraw(1) // attacker redeems their 1 share
└── assetsOut = (1 * 1_500_000) / 1 = 1,500,000 USDC
6. repay flash loan: 1_000_000 + fee
PROFIT: ~499,991 USDC at victim's expense
TRANSACTION END
6. Economic Invariant Violations
Beyond oracle manipulation and governance capture, flash loans enable a subtler class of attacks: violations of a protocol’s economic invariants — the mathematical relationships that must hold for it to remain solvent.
A malicious flash loan attack transaction typically contains a sequence of actions (function calls to smart contracts). The first action borrows a very large sum of digital assets from a flash loan contract and the last action returns the borrowed assets. The sequence of actions in the middle interacts with multiple DeFi contracts using the borrowed assets to exploit their design flaws. When a DeFi contract fails to consider corner cases caused by the large sum of borrowed assets, the attacker may extract prohibitive profits.
Classic invariants that flash loans stress-test:
- AMM
x * y = k: Should hold before and after any swap, but edge cases at extreme trade sizes can produce rounding errors exploitable at scale. - Lending health factor ≥ 1: Must be satisfied at the end of every operation. A protocol that checks health factor only at the start of a function, not the end, can be drained through intermediate states.
- Total shares × price = total assets: Share-token protocols must ensure no accounting path exists that divorces these quantities.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title SafeLender — enforces invariant after every state change
contract SafeLender {
// ... state variables ...
modifier invariantCheck() {
_;
// Post-condition: every account must be solvent after the call
require(
_totalCollateralValue() >= _totalDebtValue() * MIN_COLLATERAL_RATIO / 1e18,
"INVARIANT: undercollateralised system"
);
}
function borrow(uint256 amount) external invariantCheck {
// borrow logic ...
}
function withdraw(uint256 collateralAmount) external invariantCheck {
// withdrawal logic ...
}
function _totalCollateralValue() internal view returns (uint256) {
// Uses TWAP oracle, not spot price
return _getTWAPPrice() * totalCollateral / 1e18;
}
}
The invariantCheck modifier is the programmatic statement that no individual operation may leave the system in an illegal state, regardless of the size of the capital deployed.
7. Defenses
7.1 TWAP Oracles
To mitigate price manipulation using sudden spot price changes, one can use a TWAP. Instead of taking an instant snapshot (spot price), a TWAP averages prices over a defined period, typically ranging from minutes to hours. This significantly blunts the impact of short-term manipulations.
To significantly skew a TWAP over its window, an attacker must sustain a distorted price for the duration of that window. This is a considerably more costly feat than a fleeting flash loan manipulation. Especially in high-liquidity pools, the trades necessary to hold a skewed price would incur substantial slippage or require immense capital.
The TWAP approach is particularly effective for applications that can tolerate some price latency, such as settlement layers, treasury operations, or slow-moving markets. However, the time window is a crucial design parameter: a longer window increases manipulation resistance but introduces more latency in price updates; a shorter window is quicker but easier to manipulate.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title TWAPOracle — manipulation-resistant price feed using Uniswap V3
contract TWAPOracle {
IUniswapV3Pool public immutable pool;
uint32 public immutable twapInterval; // seconds, e.g. 1800 = 30 min
constructor(address _pool, uint32 _twapInterval) {
pool = IUniswapV3Pool(_pool);
twapInterval = _twapInterval;
}
/// @notice Returns the time-weighted average price of token0 in terms of token1.
/// @dev Uses Uniswap V3 OracleLibrary — resistant to single-block manipulation.
function getPrice() external view returns (uint256 price) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapInterval; // start of window
secondsAgos[1] = 0; // now
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0];
int24 meanTick = int24(tickCumulativeDelta / int56(uint56(twapInterval)));
// Convert tick → price (token0 per token1, scaled 1e18)
// OracleLibrary.getQuoteAtTick handles this safely:
price = OracleLibrary.getQuoteAtTick(
meanTick,
1e18, // baseAmount
pool.token0(),
pool.token1()
);
}
}
Oracles like Chainlink, which are decentralised in nature, are significantly harder to break since the attacker would have to manipulate 50%+1 of nodes on a price feed. For production protocols handling significant TVL, aggregating across Chainlink and a TWAP is the gold standard.
7.2 Snapshot Voting and Governance Timelocks
The Beanstalk lesson is unambiguous: being able to vote on and execute a proposal in the same transaction is a functionality that leaves a DAO vulnerable to a governance attack, due to the nature of flash loans.
The remedy has two components:
1. Historical snapshot voting. Measure voting power at a block number in the past (typically the block before the proposal was created), not at the moment of voting. This ensures that tokens borrowed inside the voting transaction have zero weight, because the snapshot predates the loan.
2. Execution timelocks. Even if a proposal passes legitimately, insert a mandatory delay (24–48 hours minimum) between passage and execution. By requiring long-term token locking to acquire voting rights, protocols like Curve render short-term speculative control infeasible and align influence with protocol loyalty.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title SnapshotGovernance — flash-loan-resistant voting
contract SnapshotGovernance {
ERC20Votes public immutable token;
uint256 public constant TIMELOCK_DELAY = 2 days;
uint256 public constant SNAPSHOT_LAG = 1; // blocks before proposal
struct Proposal {
uint256 snapshotBlock; // voting power measured HERE
uint256 forVotes;
uint256 againstVotes;
uint256 executableAfter; // TIMELOCK_DELAY after passage
bool executed;
address target;
bytes callData;
}
mapping(uint256 => Proposal) public proposals;
uint256 public nextId;
constructor(address _token) {
token = ERC20Votes
(_token) {
token = ERC20Votes(_token);
}
function propose(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
string calldata description
) external returns (uint256 id) {
id = nextId++;
proposals[id] = Proposal({
targets: targets,
values: values,
calldatas: calldatas,
description: description,
snapshotBlock: block.number, // ❌ current block — flash loan window
voteStart: block.number + 1,
voteEnd: block.number + VOTING_PERIOD,
executed: false
});
}
function castVote(uint256 proposalId, bool support) external {
Proposal storage p = proposals[proposalId];
uint256 votes = token.getPastVotes(msg.sender, p.snapshotBlock);
// ❌ if proposer borrowed tokens and snapshot is current block,
// votes reflects the borrowed balance
if (support) p.forVotes += votes;
else p.againstVotes += votes;
}
}
The fix: snapshot voting power at block.number - 1 or enforce a proposal delay so the snapshot block is always in the past relative to the moment of proposal creation. EIP-6372 and OpenZeppelin Governor’s proposalSnapshot are built around this principle.
Defenses Against Flash Loan Attacks
TWAP Oracles
The most fundamental defense against price-based flash loan attacks. A time-weighted average price cannot be moved in a single block because it accumulates over many blocks. An attacker who spends one block manipulating a Uniswap V3 pool shifts the current tick but contributes only a fraction of the TWAP — proportional to how long they sustain the manipulation relative to the window.
For a 30-minute TWAP with an adversary who controls one block: the attacker can shift the TWAP by at most 1 / (30 * 5) = 0.67% per block (assuming 12-second blocks). Making that manipulation profitable requires the resulting borrow to exceed the cost of the price move — typically infeasible for liquid pairs.
Snapshot Voting
OpenZeppelin’s Governor enforces a proposalDelay between when a proposal is created and when voting begins. The snapshot is taken at the proposal creation block. Because the proposer must create the proposal before borrowing (or sustain the borrow for the entire delay period), flash loan governance attacks become economically infeasible.
// OpenZeppelin GovernorSettings
uint256 public constant PROPOSAL_DELAY = 1 days; // 1 day before voting starts
uint256 public constant VOTING_PERIOD = 7 days;
With a 1-day proposal delay, an attacker would need to hold borrowed tokens for a full day before they can influence votes — impossible with flash loans, which require repayment within a single transaction.
Reentrancy Guards
Flash loan callbacks are reentrancy vectors. Any protocol that implements a flashLoan function must ensure that the callback cannot re-enter the protocol in a way that creates inconsistent state.
// Standard reentrancy guard on the flash loan callback entry point
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external override nonReentrant returns (bool) {
// Callback logic — cannot re-enter this contract
return true;
}
Invariant Enforcement
The most robust defense is designing protocols where the critical invariant cannot be violated regardless of the path taken to reach any state. For a lending protocol, the invariant is that no position is undercollateralized at the end of any transaction. If this invariant is checked atomically (e.g., via a health factor check that executes after all operations in a transaction), a flash loan attack that temporarily distorts collateral values will fail at the invariant check even if it temporarily satisfies intermediate conditions.
Flash Loan Attack Checklist
Oracle surfaces
- No price feed reads spot price from an AMM without a TWAP
- TWAP window is long enough that single-block manipulation cannot move it profitably
- All oracle reads happen after external calls are complete (no stale reads)
Governance surfaces
- Voting power snapshot is taken at a block in the past, not the current block
- Proposal delay prevents flash-borrowed tokens from influencing the snapshot
- Quorum is not achievable with a single large flash-borrowed position
Collateral and accounting
- Collateral valuation uses oracle prices that cannot be moved in one block
- Health factor checks are atomic with the operation that changes collateral
- No intermediate state within a transaction allows undercollateralized borrowing
Flash loan callbacks
- All
flashLoancallback entry points havenonReentrant - Callback cannot re-enter any function that reads state modified during the flash loan
- The loaned amount is verified as repaid before any post-loan state finalizes
Economic invariants
- Core protocol invariant (e.g., total collateral value ≥ total debt) is checked at the end of every transaction that modifies either side
- There is no path through which a flash loan can leave the protocol in an insolvent state, even temporarily