Development of Flash Loan Attack Protection System
Flash loan is an uncollateralized loan that must be repaid in the same transaction. If not repaid—the entire transaction reverts. From the protocol's perspective (Aave, Uniswap V3), it's a risk-free operation: either the money comes back or the transaction doesn't happen.
The problem isn't with flash loans themselves—they're a legitimate tool for arbitrage, liquidations, refinancing. The problem is that they give the attacker temporary access to enormous capital (hundreds of millions of dollars) without collateral. If the protocol makes economic decisions based on easily manipulable data (DEX spot price, non-TWAP oracle)—one transaction with flash loan can profit the attacker millions.
Beanstalk ($182M, 2022), Cream Finance ($130M, 2021), Mango Markets ($114M, 2022)—all hacks used temporary capital control. Common thread: protocols used data that could be shifted in one transaction.
Anatomy of Flash Loan Attack
Understanding the attack vector is necessary for building defense. Typical attack consists of four steps:
1. Take flash loan (e.g., 100M USDC from Aave)
2. Manipulate state (pump/dump price in DEX pool)
3. Exploit protocol (which reads manipulated data)
4. Repay flash loan + fee, keep profit
Concrete example—price oracle manipulation:
1. Flash loan: 50M DAI
2. Dump DAI into Uniswap V2 DAI/ETH pool (spot price DAI falls)
3. Call protocol which reads Uniswap V2 spot price for collateral valuation
→ Collateral in DAI now "cheaper", can get discount on liquidation
or value debt in DAI as smaller
4. Profit → repay flash loan
Another type—governance flash loan:
1. Flash loan governance tokens
2. Momentary proposal creation + voting with enormous weight
3. Execute proposal (drain treasury)
4. Repay flash loan
(This is how Beanstalk was attacked—attacker with one governance vote passed a proposal to transfer treasury to themselves.)
Protection 1: Price Oracle—TWAP Instead of Spot
This is the most common and critical protection.
Why Spot Price is Vulnerable
Uniswap V2/V3 spot price = current reserve ratio. Large swap in pool instantly changes spot price. In one transaction, you can shift price by 50–80% in a pool with moderate liquidity.
TWAP (Time-Weighted Average Price)
TWAP—arithmetic average of price over period. Uniswap V2/V3 stores cumulative price accumulators from which TWAP over arbitrary period can be calculated.
contract TWAPOracle {
IUniswapV3Pool public pool;
uint32 public constant TWAP_PERIOD = 30 minutes;
function getTWAP() external view returns (uint256 price) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = TWAP_PERIOD; // 30 minutes ago
secondsAgos[1] = 0; // now
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(TWAP_PERIOD)));
// Convert tick to price
price = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
// ... conversion sqrtPrice → human-readable
}
}
Choosing TWAP period—critical parameter. Too short (1–5 minutes)—attacker with sufficient capital can hold manipulated price for several blocks. Too long (4–8 hours)—TWAP lags significantly during volatile periods, causing incorrect liquidations.
Practice: 30 minutes is reasonable default for most DeFi protocols. For highly volatile assets—1–2 hours.
Chainlink as Primary Oracle
Chainlink price feed—aggregated price from many independent nodes with heartbeat update. Manipulation requires compromising majority of oracle nodes—economically infeasible.
contract PriceConsumer {
AggregatorV3Interface public priceFeed;
uint256 public constant HEARTBEAT = 3600; // 1 hour
uint256 public constant MAX_STALENESS = HEARTBEAT * 2; // 2 hours—maximum
function getPrice() external view returns (uint256) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Check staleness: data not older than MAX_STALENESS
require(block.timestamp - updatedAt <= MAX_STALENESS, "Stale price");
// Check round correctness
require(answeredInRound >= roundId, "Stale round");
// Check price positivity
require(answer > 0, "Invalid price");
return uint256(answer);
}
}
General recommendation: use Chainlink as primary oracle, Uniswap TWAP as sanity check. If two sources diverge by more than X%—pause operations.
Protection 2: Snapshot Voting Power
Governance flash loan attacks exploit the fact that voting power = current token balance. ERC-20Votes solves this through checkpoint system.
// Voting power fixed at snapshot block (before voting begins)
uint256 votePower = token.getPastVotes(voter, proposalSnapshot);
// Flash loan AFTER snapshot doesn't give voting power
// Flash loan BEFORE snapshot requires holding borrowed tokens through voting delay
Voting delay—minimum period between proposal creation and voting start. If voting delay = 2 days, attacker must hold borrowed tokens 2 days—economically unfavorable (fee + opportunity cost).
// OpenZeppelin Governor
constructor(...) GovernorSettings(
2 days, // votingDelay—protection from flash loan governance attacks
5 days, // votingPeriod
threshold
) {}
Beanstalk was attacked precisely because it didn't use voting delay: proposal could be created and executed in one transaction.
Protection 3: Reentrancy Guard and Same-Block Checks
Some flash loan attacks exploit reentrancy or same-block state manipulation.
Same-Block Checks
contract Vault {
mapping(address => uint256) private _depositBlock;
function deposit(uint256 amount) external {
_depositBlock[msg.sender] = block.number;
// ...
}
function withdraw(uint256 amount) external {
// Cannot deposit and withdraw in same block
require(
_depositBlock[msg.sender] < block.number,
"Flash loan protection: same block"
);
// ...
}
}
This blocks pattern: flash_loan → deposit → call function that reads vault balance → withdraw → repay_loan.
Downside: legitimate users also can't deposit+withdraw in one block. For most protocols this is acceptable.
Nonreentrant + View Functions
nonReentrant protects state-changing functions from reentrancy. But view functions aren't protected—they can be called from middle of another transaction.
If view function is used by external protocol to get price or TVL—state manipulation via reentrancy changes what this view function sees.
// VULNERABLE: state can be manipulated via reentrancy
function getSharePrice() external view returns (uint256) {
return totalAssets() * 1e18 / totalSupply();
}
// totalAssets() reads contract balance—which can be temporarily inflated
function totalAssets() public view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}
Solution: store cached total assets value, updated only in protected functions.
Protection 4: Circuit Breakers and Rate Limiting
Maximum Volume Per Transaction
uint256 public constant MAX_SINGLE_DEPOSIT = 1_000_000e6; // $1M max
function deposit(uint256 amount) external {
require(amount <= MAX_SINGLE_DEPOSIT, "Exceeds single tx limit");
// ...
}
Flash loan attacks usually operate on hundreds of millions. Limiting single transaction reduces maximum damage from any attack.
Pause Mechanism with Auto-Trigger
contract ProtectedProtocol is Pausable {
uint256 public lastTVL;
uint256 public constant TVL_DROP_THRESHOLD = 20; // 20% per transaction
modifier checkTVLAnomaly() {
uint256 tvlBefore = totalValueLocked();
_;
uint256 tvlAfter = totalValueLocked();
if (tvlBefore > 0) {
uint256 dropPercent = ((tvlBefore - tvlAfter) * 100) / tvlBefore;
if (dropPercent > TVL_DROP_THRESHOLD) {
_pause();
emit EmergencyPause(tvlBefore, tvlAfter, dropPercent);
}
}
}
}
Circuit breaker: if TVL drops more than N% in one transaction—protocol auto-pauses. This doesn't prevent attack but limits its scale.
Time-Weighted Balances
Instead of current balance, use time-weighted average balance for critical calculations:
// ERC-20Votes checkpoint approach applied to liquidity
function getTimeWeightedLiquidity(address provider, uint256 lookback)
external view returns (uint256)
{
// Averaged liquidity over lookback period
// Manipulation in one transaction minimally affects average
}
On-Chain Monitoring
Defense system is incomplete without monitoring. Forta Network—decentralized detection network with bots monitoring on-chain activity.
// Forta bot: detect potential flash loan attack
async function handleTransaction(txEvent) {
const findings = [];
// Check for flash loan calldata in transaction
const flashLoanCalls = txEvent.filterFunction([
'flashLoan(address,address,uint256,bytes)',
'flash(address,address,uint256,uint256,bytes)'
]);
if (flashLoanCalls.length > 0) {
// Check for significant state changes of our protocol
const protocolEvents = txEvent.filterLog(PROTOCOL_EVENTS, PROTOCOL_ADDRESS);
if (protocolEvents.length > 0) {
findings.push(Finding.fromObject({
name: "Flash loan + protocol interaction",
description: `Flash loan detected in same tx as protocol events`,
alertId: "FLASH-LOAN-INTERACTION",
severity: FindingSeverity.Medium,
type: FindingType.Suspicious
}));
}
}
return findings;
}
Forta alerts can be sent to PagerDuty / Telegram via webhook, giving team 1–2 minutes to respond before attack spreads.
Comprehensive Defense Architecture
No single measure is sufficient. Effective protection system—is layers:
| Level | Mechanism | Protects From |
|---|---|---|
| Oracle | Chainlink primary + TWAP sanity | Price manipulation |
| Governance | Voting delay (2+ days) + ERC-20Votes | Flash loan governance |
| State | Same-block check on withdraw | Deposit-exploit-withdraw |
| Flow | Max per-tx limits | Damage limitation |
| Circuit breaker | Auto-pause on TVL anomaly | Early stop on attack |
| Monitoring | Forta bots | Detection and alerting |
Developing comprehensive protection system: audit existing oracle dependencies and governance mechanisms—1 week, develop protective contracts—2–3 weeks, integrate monitoring—1 week, attack tests in fork environment—2 weeks.
Cost depends on protocol complexity and number of oracle integration points.







