In Web3, you can borrow $100 million with zero collateral.
And 15 seconds later, you can be walking away with a $2 million profit.
This isn't a "get rich quick" scheme; it's a Flash Loan Attack.
This is one of the most fascinating and feared exploits in DeFi. It’s not a "bug" in the traditional sense. It’s a feature, used as a weapon against a protocol's flawed economic logic. In this deep dive, we'll dissect exactly how these attacks work, look at the code, and understand how to defend against them.
Part 1: What is a Flash Loan? (The "Weapon")
Before it's an "attack," it's a "flash loan."
A flash loan is a feature offered by protocols like Aave and Uniswap. It allows you to borrow a massive amount of assets (e.g., $200M of ETH) with zero collateral.
The catch? You must repay the loan, plus a small fee, within the exact same transaction.
This is possible because of a core property of the blockchain called atomicity. A transaction is "atomic," meaning it either completes 100% (all steps succeed) or it fails and reverts 100% (as if it never happened).
So, for an attacker, a flash loan is a "risk-free" source of infinite capital.
- If their attack succeeds: They repay the loan and keep the millions in profit.
- If their attack fails: The entire transaction reverts, including the initial loan. They lose nothing (except gas fees).
Part 2: The Vulnerability (The "Weak Point")
A flash loan is just a tool. The real vulnerability is almost always Price Oracle Manipulation.
An oracle is how a smart contract asks, "What is the price of ETH right now?"
A bad protocol design is to get this price from a single, easily-manipulated source. The most common mistake? Using a single DEX liquidity pool (like a Uniswap V2 pool) as your price oracle.
Why is this a vulnerability? The price in a simple DEX pool is just a (Reserve A / Reserve B) formula. With enough capital (which a flash loan provides), an attacker can temporarily buy all of one asset, drastically skewing this price for one block.
Let's invent our vulnerable protocol:
- PoorLenderBank.sol: A simple lending protocol.
- It lets users deposit TokenA as collateral to borrow TokenB (which is a stablecoin).
- The fatal flaw: To decide the value of TokenA, it checks the price directly from a single Uniswap V2 TokenA/TokenB pool.
Part 3: Anatomy of the Attack (Step-by-Step)
Here is the exact sequence of events, all happening inside one single transaction:
- Deploy AttackContract.sol: The attacker first deploys their own smart contract.
- Start the Attack: The attacker calls the startAttack() function on their contract.
- Step 1: Get the Weapon (Flash Loan)
- The AttackContract requests a massive flash loan of, say, 10,000,000 TokenB (stablecoins) from Aave or Uniswap.
- Step 2: Manipulate the Oracle
- The AttackContract takes all 10M TokenB and swaps them for TokenA on the vulnerable Uniswap pool that PoorLenderBank trusts.
- This single massive "buy" order drains the pool of TokenA and floods it with TokenB.
- The price of TokenA (relative to TokenB) on this pool skyrockets 1000x. (Reserve A / Reserve B) is now a huge number.
- Step 3: Exploit the Vulnerable Protocol
- The AttackContract now goes to PoorLenderBank.sol.
- It deposits a tiny amount of TokenA (which it just bought) as collateral.
- PoorLenderBank checks the price of TokenA to see how much it's worth. It asks the manipulated Uniswap pool.
- The pool replies, "This TokenA is worth 1000x its normal value!"
- PoorLenderBank now believes the attacker's tiny TokenA deposit is "worth" millions.
- The attacker says, "Great. Based on my valuable collateral, I'd like to borrow all of your TokenB reserves."
- PoorLenderBank agrees and sends its entire treasury (e.g., 12,000,000 TokenB) to the AttackContract.
- Step 4: Clean Up & Repay
- The AttackContract now has 12,000,000 TokenB.
- It only needs ~10,000,000 TokenB to repay its loan.
- It uses a portion of its stolen funds to swap back for TokenA (which restores the pool price, not that it matters).
- It repays the initial 10,000,000 TokenB flash loan + the small fee.
- Step 5: Profit
- The attacker is left with ~2,000,000 TokenB (stablecoins) as pure profit.
- The transaction completes successfully. PoorLenderBank is left with worthless TokenA collateral and an empty treasury.
Part 4: The Code (Simplified)
This is what the logic looks like.
1. The Vulnerable Contract's Flaw (PoorLenderBank.sol)
// DO NOT USE THIS CODE. IT IS VULNERABLE.
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
contract PoorLenderBank {
IUniswapV2Pair public vulnerableOracle; // e.g., The ETH/USDC pool
// ... other lending logic ...
/**
* THE FATAL FLAW:
* This function gets the price from a single, manipulatable source.
*/
function getCollateralValueInUSD(address _token, uint _amount) public view returns (uint) {
// (reserves.reserve0, reserves.reserve1, ) = vulnerableOracle.getReserves();
// This math is easily manipulated by a flash loan
// uint price = reserves.reserve1 / reserves.reserve0;
// return _amount * price;
// This will return a massively inflated value during an attack.
}
}
2. The Attack Contract's Logic (AttackContract.sol)
This is a conceptual example of an attack contract using Aave's executeOperation callback.
import { ILendingPool, ILendingPoolAddressesProvider } from "@aave/protocol-v2/contracts/interfaces/ILendingPool.sol";
contract AttackContract {
address public owner;
ILendingPool public aavePool; // Aave V2 Lending Pool
constructor(address _addressesProvider) {
owner = msg.sender;
aavePool = ILendingPool(ILendingPoolAddressesProvider(_addressesProvider).getLendingPool());
}
/**
* This is the entry point for the attack.
* 1. Takes the Flash Loan.
* 2. Triggers the main attack logic in executeOperation().
*/
function startAttack(address _loanAsset, uint _amount) external {
// Request the flash loan from Aave
aavePool.flashLoan(
address(this), // receiverAddress
new address[] {_loanAsset},
new uint[] {_amount},
new uint[] {0}, // modes (0 for stable rate)
address(this), // onBehalfOf
bytes(""), // params
0 // referralCode
);
}
/**
* Aave calls this function *after* giving us the money.
* This is where the step-by-step attack happens.
*/
function executeOperation(
address[] calldata assets,
uint[] calldata amounts,
uint[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
// Step 2: Manipulate the Oracle
// ... code to swap(amounts[0]) on the vulnerable Uniswap pool ...
// Step 3: Exploit the Vulnerable Protocol
// ... code to deposit collateral and borrow from PoorLenderBank ...
// Step 4: Clean Up & Repay
// ... code to get back the original asset (if needed) ...
uint amountToRepay = amounts[0] + premiums[0];
// Ensure we have enough to repay Aave
require(IERC20(assets[0]).balanceOf(address(this)) >= amountToRepay, "Failed to profit");
// Approve Aave to take back the loan + fee
IERC20(assets[0]).approve(address(aavePool), amountToRepay);
return true; // Signal success
}
// Function to withdraw profits
function withdraw() external {
require(msg.sender == owner, "Not owner");
// ... logic to send all remaining (stolen) tokens to owner ...
}
// We need a fallback to receive ETH if we borrow/steal it
receive() external payable {}
}
Part 5: The Mitigation (The "Shield")
How do we stop this? NEVER trust a single, on-chain DEX pool for price data.
This is a solved problem.
Solution 1: Use Chainlink Price Feeds (The Standard)
Chainlink is a decentralized oracle network. It gathers price data from dozens of off-chain exchanges, aggregates it, and puts it on-chain in a way that cannot be manipulated by a single transaction.
// THE SECURE WAY
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecureLenderBank {
AggregatorV3Interface internal priceFeed;
constructor() {
// Set to the official ETH/USD feed.
priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
}
/**
* THE FIX:
* This function gets the price from a secure, decentralized oracle.
* It is not vulnerable to flash loan manipulation.
*/
function getCollateralValueInUSD() public view returns (int) {
(
, // roundId
int price,
, // startedAt
, // updatedAt
// answeredInRound
) = priceFeed.latestRoundData();
// Returns a secure price, e.g., 3000 * 10**8
return price;
}
}
Solution 2: Use Time-Weighted Average Prices (TWAPs)
If you must get a price from a DEX, you can't use the "spot price." Instead, you must use a TWAP (Time-Weighted Average Price). Uniswap V2 and V3 have this feature built-in.
A TWAP calculates the average price over a period of time (e.g., the last 30 minutes). A flash loan attack happens in one block (12 seconds). It cannot influence the average price over 30 minutes, making it secure from this attack vector.
Conclusion
Flash loans are a powerful and neutral tool. They created an entire industry of "DeFi arbitrage." But in the hands of an attacker, they are the ultimate weapon for punishing lazy or naive protocol design.
For us as security professionals, the lesson is clear: Your protocol is only as secure as your price oracle. Never, ever, trust a single, manipulatable source for your data.
Disclaimer: All code provided is for educational, illustrative purposes only. It is simplified and not production-ready. Do not use the vulnerable code.
Top comments (0)