AdvancedCritical Severity

Flash Loan Attack Vectors

15 min readUpdated Dec 12, 2024By Hexific Team

What are Flash Loans?

Flash loans are uncollateralized loans that must be borrowed and repaid within a single transaction. If the loan isn't repaid by the end of the transaction, the entire transaction reverts as if it never happened.

💡 Key Characteristics

  • No collateral required - Borrow millions with zero upfront capital
  • Atomic execution - Everything happens in one transaction
  • All-or-nothing - If repayment fails, loan never happened
  • Available on: Aave, dYdX, Uniswap V2/V3, Balancer
FlashLoanExample.sol
1// Basic flash loan structure
2interface IFlashLoanReceiver {
3    function executeOperation(
4        address[] calldata assets,
5        uint256[] calldata amounts,
6        uint256[] calldata premiums,
7        address initiator,
8        bytes calldata params
9    ) external returns (bool);
10}
11
12contract FlashLoanExample is IFlashLoanReceiver {
13    IPool public aavePool;
14    
15    function executeFlashLoan(address asset, uint256 amount) external {
16        address[] memory assets = new address[](1);
17        assets[0] = asset;
18        
19        uint256[] memory amounts = new uint256[](1);
20        amounts[0] = amount;
21        
22        // 0 = no debt, 1 = stable, 2 = variable
23        uint256[] memory modes = new uint256[](1);
24        modes[0] = 0;
25        
26        aavePool.flashLoan(
27            address(this),
28            assets,
29            amounts,
30            modes,
31            address(this),
32            "",
33            0
34        );
35    }
36    
37    function executeOperation(
38        address[] calldata assets,
39        uint256[] calldata amounts,
40        uint256[] calldata premiums,
41        address initiator,
42        bytes calldata params
43    ) external returns (bool) {
44        // Your attack/arbitrage logic here
45        
46        // Repay the loan + premium
47        uint256 amountOwed = amounts[0] + premiums[0];
48        IERC20(assets[0]).approve(address(aavePool), amountOwed);
49        
50        return true;
51    }
52}

Common Attack Vectors

Flash loans themselves aren't vulnerabilities - they're a feature. However, they amplify other vulnerabilities by providing attackers with massive capital. Here are the main vectors:

🎯 Price Oracle Manipulation

Temporarily manipulate spot prices to trick protocols using on-chain price feeds.

🗳️ Governance Attacks

Borrow governance tokens to pass malicious proposals in a single block.

💱 Arbitrage Exploitation

Exploit pricing discrepancies between DEXs or lending protocols.

🏦 Collateral Manipulation

Inflate collateral value to borrow more than should be allowed.

Price Oracle Manipulation

The most common flash loan attack involves manipulating prices on AMMs (Automated Market Makers) that protocols use as price oracles.

🚨

Attack Pattern

  1. Flash loan a large amount of Token A
  2. Swap Token A for Token B on a DEX (crashes Token A price)
  3. Interact with vulnerable protocol using manipulated price
  4. Profit from mispriced assets or liquidations
  5. Swap back and repay flash loan
PriceOracleComparison.sol
1// ❌ VULNERABLE: Using spot price from AMM
2contract VulnerableLending {
3    IUniswapV2Pair public pair;
4    
5    function getPrice() public view returns (uint256) {
6        (uint112 reserve0, uint112 reserve1,) = pair.getReserves();
7        // This can be manipulated within a single transaction!
8        return (uint256(reserve1) * 1e18) / uint256(reserve0);
9    }
10    
11    function borrow(uint256 collateralAmount) external {
12        uint256 price = getPrice(); // Manipulable!
13        uint256 borrowLimit = (collateralAmount * price) / 1e18;
14        // Attacker can inflate price to borrow more
15        _transferTokens(msg.sender, borrowLimit);
16    }
17}
18
19// ✅ SECURE: Using Chainlink or TWAP
20contract SecureLending {
21    AggregatorV3Interface public priceFeed;
22    
23    function getPrice() public view returns (uint256) {
24        (
25            ,
26            int256 price,
27            ,
28            uint256 updatedAt,
29            
30        ) = priceFeed.latestRoundData();
31        
32        // Check for stale data
33        require(block.timestamp - updatedAt < 1 hours, "Stale price");
34        require(price > 0, "Invalid price");
35        
36        return uint256(price);
37    }
38}

Governance Attacks

Flash loans can be used to temporarily acquire enough governance tokens to pass malicious proposals or manipulate voting outcomes.

🚨 Beanstalk Attack (April 2022)

Attacker used $1B in flash loaned funds to acquire enough STALK tokens to pass a malicious governance proposal that drained $182M from the protocol.

Attack flow: Flash loan → Deposit into Beanstalk → Vote on malicious BIP → Execute BIP → Drain funds → Repay loan
GovernanceComparison.sol
1// ❌ VULNERABLE: No snapshot or timelock
2contract VulnerableGovernance {
3    mapping(address => uint256) public votingPower;
4    
5    function vote(uint256 proposalId, bool support) external {
6        // Uses current balance - can be manipulated!
7        uint256 votes = token.balanceOf(msg.sender);
8        proposals[proposalId].votes += votes;
9    }
10    
11    function execute(uint256 proposalId) external {
12        // No timelock - immediate execution
13        require(proposals[proposalId].votes > quorum, "Not passed");
14        _executeProposal(proposalId);
15    }
16}
17
18// ✅ SECURE: Snapshot voting + timelock
19contract SecureGovernance {
20    function vote(uint256 proposalId, bool support) external {
21        // Use snapshot from proposal creation block
22        uint256 snapshotBlock = proposals[proposalId].snapshotBlock;
23        uint256 votes = token.getPastVotes(msg.sender, snapshotBlock);
24        
25        proposals[proposalId].votes += votes;
26    }
27    
28    function execute(uint256 proposalId) external {
29        require(proposals[proposalId].votes > quorum, "Not passed");
30        // Timelock prevents immediate execution
31        require(
32            block.timestamp >= proposals[proposalId].eta,
33            "Timelock not expired"
34        );
35        _executeProposal(proposalId);
36    }
37}

Real-World Examples

Euler Finance (March 2023)

$197M

Flash loan + donation function exploit. Attacker used flash loans to create leveraged positions, then donated to make themselves liquidatable at a profit.

Flash LoanDonation Attack

Beanstalk (April 2022)

$182M

Flash loan governance attack. Borrowed $1B to gain voting majority and pass a malicious proposal in a single transaction.

Flash LoanGovernance

Cream Finance (October 2021)

$130M

Flash loan + price oracle manipulation. Exploited the pricing of yUSD vault tokens to drain lending pools.

Flash LoanOracle

Pancake Bunny (May 2021)

$45M

Flash loan used to manipulate BUNNY/BNB price, then exploit the protocol's reward calculation mechanism.

Flash LoanPrice Manipulation

Prevention Strategies

✅ Use Time-Weighted Oracles

TWAP (Time-Weighted Average Price) oracles average prices over time, making single-block manipulation impractical.

✅ Chainlink Price Feeds

Decentralized oracle networks aggregate data from multiple sources, resistant to on-chain manipulation.

✅ Governance Timelocks

Delay between proposal passing and execution gives community time to react to malicious proposals.

✅ Snapshot Voting

Use token balances from a past block (snapshot) for voting power, not current balance.

✅ Multi-Block Validation

Require actions to span multiple blocks, making atomic flash loan attacks impossible.

✅ Flash Loan Guards

Detect and block interactions from contracts that borrowed via flash loans in the same transaction.

FlashLoanDefenses.sol
1// Flash loan detection guard
2contract FlashLoanGuard {
3    mapping(address => uint256) private _lastBlock;
4    
5    modifier noFlashLoan() {
6        require(
7            _lastBlock[tx.origin] != block.number,
8            "Flash loan detected"
9        );
10        _lastBlock[tx.origin] = block.number;
11        _;
12    }
13    
14    // Alternative: Check if caller is a contract with code
15    modifier noContract() {
16        require(msg.sender == tx.origin, "No contracts");
17        _;
18    }
19}
20
21// TWAP Oracle implementation
22contract TWAPOracle {
23    uint256 public constant PERIOD = 30 minutes;
24    
25    uint256 public price0CumulativeLast;
26    uint256 public price1CumulativeLast;
27    uint32 public blockTimestampLast;
28    
29    FixedPoint.uq112x112 public price0Average;
30    FixedPoint.uq112x112 public price1Average;
31    
32    function update() external {
33        (
34            uint256 price0Cumulative,
35            uint256 price1Cumulative,
36            uint32 blockTimestamp
37        ) = UniswapV2OracleLibrary.currentCumulativePrices(pair);
38        
39        uint32 timeElapsed = blockTimestamp - blockTimestampLast;
40        require(timeElapsed >= PERIOD, "Period not elapsed");
41        
42        price0Average = FixedPoint.uq112x112(
43            uint224((price0Cumulative - price0CumulativeLast) / timeElapsed)
44        );
45        price1Average = FixedPoint.uq112x112(
46            uint224((price1Cumulative - price1CumulativeLast) / timeElapsed)
47        );
48        
49        price0CumulativeLast = price0Cumulative;
50        price1CumulativeLast = price1Cumulative;
51        blockTimestampLast = blockTimestamp;
52    }
53}