Auto-loaded by flashloan-auditor and defi-auditor agents during Phase 2...
2025 Statistics: Flash loans = 83.3% of DeFi exploits, Oracle manipulation = +31% YoY, Price manipulation = 34.3% of MUBs.
CRITICAL: With flash loans, attackers have INFINITE CAPITAL for one transaction.
+------------------------------------------------------------------+
| SINGLE ATOMIC TRANSACTION |
+------------------------------------------------------------------+
| 1. BORROW | Flash loan $100M+ from Aave/dYdX (cost: 0.09%) |
| 2. MANIPULATE| Change any on-chain value (price, balance, ratio) |
| 3. EXPLOIT | Call target function with manipulated state |
| 4. PROFIT | Extract value (mint, borrow, swap at bad rate) |
| 5. REPAY | Return flash loan + fee |
| 6. KEEP | Attacker keeps profit, victims lose funds |
+------------------------------------------------------------------+
Key insight: If ANY step fails, entire transaction reverts. Attacker loses only gas (~$50). This means attackers can try complex attacks with zero risk.
Protocol trusts ONE price source that attacker can manipulate.
// VULNERABLE: Single DEX pool as price source
function getPrice() public view returns (uint256) {
(uint112 r0, uint112 r1,) = uniswapPair.getReserves();
return uint256(r1) * 1e18 / uint256(r0); // @audit Flash loan can drain r0
}
Attacker's view: "I can move this price with enough capital. Flash loan gives me that capital."
Using current-moment values instead of time-averaged values.
// VULNERABLE: Current block's reserves
price = reserves[1] / reserves[0]; // @audit Reflects THIS transaction's state
// What attacker sees:
// 1. Swap to move reserves
// 2. Read manipulated price
// 3. Exploit protocol
// 4. Swap back
// All in one tx!
Detection: Any getReserves(), slot0(), balanceOf() used for pricing is suspect.
Oracle price could be hours or days old, but protocol uses it anyway.
// VULNERABLE: No staleness check
(, int256 price,,,) = chainlinkFeed.latestRoundData();
return uint256(price); // @audit Could be from yesterday!
// Attacker's opportunity:
// - Wait for oracle to become stale during volatility
// - Real price moved 20%, oracle still shows old price
// - Liquidate users at wrong price, or borrow too much
Detection: latestRoundData() without checking updatedAt timestamp.
Protocol blindly trusts external protocol's reported values.
// VULNERABLE: Trusting external vault's totalAssets
function getCollateralValue(address user) view returns (uint256) {
uint256 shares = externalVault.balanceOf(user);
uint256 pricePerShare = externalVault.totalAssets() / externalVault.totalSupply();
return shares * pricePerShare; // @audit totalAssets can be donated to!
}
Attacker's view: "I can donate to that vault and inflate pricePerShare, then borrow against it."
Trace how prices flow through the system:
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Price Source β βββΊ β Reading Function β βββΊ β Critical Decisionβ
β (Oracle/DEX) β β (getPrice, etc) β β (liquidate,mint)β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β
βΌ βΌ
Manipulable? Value Transfer
- Flash loan? (funds move)
- Donation?
- Time window?
Document each price dependency:
| Function | Price Source | Manipulable? | Impact if Manipulated |
|---|---|---|---|
| liquidate() | Chainlink ETH/USD | Staleness only | Unfair liquidations |
| borrow() | Uniswap reserves | Flash loan | Undercollateralized borrow |
| mint() | vault.totalAssets() | Donation | Steal other users' deposits |
Root Cause: Single Source + Spot Price Trust
// VULNERABLE: Direct reserve ratio
function getTokenPrice() public view returns (uint256) {
(uint112 r0, uint112 r1,) = pair.getReserves();
return uint256(r1) * 1e18 / uint256(r0); // @audit Instant manipulation
}
Attack Flow:
Search Queries:
Grep("getReserves|slot0|observe", glob="**/*.sol")
Grep("reserve0|reserve1|liquidity", glob="**/*.sol")
Verification Questions:
Root Cause: Staleness Blindness
// VULNERABLE: No freshness validation
function getPrice() external view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price); // @audit Could be stale!
}
// Attack opportunity:
// During high volatility, oracle updates lag
// Real ETH = $3000, Oracle still says $2500
// Attacker borrows at $2500 collateral value
// Immediately has undercollateralized position
Search Queries:
Grep("latestRoundData|latestAnswer", glob="**/*.sol")
Grep("updatedAt|answeredInRound", glob="**/*.sol")
Verification Questions:
updatedAt checked against a max staleness?Root Cause: Insufficient time averaging
// VULNERABLE: 1 minute TWAP
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 60; // @audit Only 60 seconds!
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
Attacker's view: "I can maintain manipulated price for 60 seconds across multiple blocks if I'm a validator, or use multiple flash loans."
Safe TWAP windows:
Search Queries:
Grep("observe|consult|TWAP|twap", glob="**/*.sol")
Grep("secondsAgo|period|window", glob="**/*.sol")
Root Cause: Composability Trust + Spot Value
// VULNERABLE: totalAssets includes donations
function totalAssets() public view returns (uint256) {
return token.balanceOf(address(this)); // @audit Donatable!
}
function pricePerShare() public view returns (uint256) {
return totalAssets() * 1e18 / totalSupply(); // @audit Inflatable!
}
Attack Flow:
Search Queries:
Grep("balanceOf\\(address\\(this\\)\\)", glob="**/*.sol")
Grep("totalAssets|totalSupply", glob="**/*.sol")
Grep("pricePerShare|exchangeRate", glob="**/*.sol")
Root Cause: No minimum output enforcement
// VULNERABLE: amountOutMin = 0
router.swapExactTokensForTokens(
amountIn,
0, // @audit Sandwich target!
path,
msg.sender,
block.timestamp
);
Attack Flow (Sandwich):
Search Queries:
Grep("amountOutMin|minAmountOut|minOut", glob="**/*.sol")
Grep("swapExact|swap\\(", glob="**/*.sol")
Root Cause: Snapshot at call time
// VULNERABLE: Current balance for voting power
function propose(bytes calldata action) external {
uint256 votes = token.balanceOf(msg.sender); // @audit Current!
require(votes >= proposalThreshold);
// Attacker: flash loan tokens β propose β return
}
function vote(uint256 proposalId, bool support) external {
uint256 votes = token.balanceOf(msg.sender); // @audit Current!
proposals[proposalId].votes += votes;
// Attacker: flash loan β vote β return
}
Search Queries:
Grep("balanceOf.*vote|vote.*balanceOf", glob="**/*.sol")
Grep("propose|quorum|threshold", glob="**/*.sol")
For each value used in critical decisions:
| Source Type | Manipulation Risk | Attack Vector |
|---|---|---|
| DEX Spot (getReserves) | CRITICAL | Flash loan swap |
| Uniswap V3 slot0 | CRITICAL | Flash loan swap |
| TWAP < 10 min | HIGH | Multi-block or validator |
| TWAP 10-30 min | MEDIUM | Validator collusion |
| TWAP > 30 min | LOW | Expensive sustained attack |
| Chainlink (no staleness) | HIGH | Wait for stale price |
| Chainlink (with staleness) | LOW | Limited window |
| balanceOf(this) | HIGH | Direct donation |
# Find price sources
Grep("getReserves|slot0|observe|latestRoundData", glob="**/*.sol")
Grep("getPrice|price\\(\\)|oracle", glob="**/*.sol")
# Find manipulable values
Grep("balanceOf\\(address\\(this\\)\\)", glob="**/*.sol")
Grep("totalAssets|totalSupply|pricePerShare", glob="**/*.sol")
# Find flash loan interactions
Grep("flashLoan|flash\\(|executeOperation", glob="**/*.sol")
Grep("onFlashLoan|IERC3156", glob="**/*.sol")
# Find vulnerable swaps
Grep("amountOutMin.*=.*0|minAmount.*=.*0", glob="**/*.sol")
Grep("swapExact|swap\\(", glob="**/*.sol")
# Find governance
Grep("propose|vote|quorum|snapshot", glob="**/*.sol")
| Excuse | Attacker's Reality |
|---|---|
| "Flash loans are expensive" | 0.09% fee on $100M = $90K. Profit can be millions. |
| "Pool has high liquidity" | Higher liquidity = need bigger flash loan. Still doable. |
| "TWAP protects us" | Short TWAP < 10min is still manipulable. Check the window. |
| "No one would do this" | MEV bots automate attacks 24/7. They're already looking. |
| "Chainlink is always accurate" | Chainlink can be stale. Always check updatedAt. |
| "This is theoretical" | Cetus ($223M), Euler ($197M), Mango ($114M), KiloEx ($117M) |
| "Attack would cost too much" | Flash loan cost is near zero. Only gas at risk. |