3.11.4 Flash Loans as a Capital Primitive
Flash loans are one of DeFi's distinctive contributions to financial primitives. A user can borrow tens of millions of dollars without collateral, use it within a single transaction, and repay before the transaction ends. The mechanism has no analog in traditional finance, where uncollateralized loans require trust and credit assessment. On-chain, the atomic-transaction guarantee — either the loan is repaid or the entire transaction reverts — eliminates default risk, which eliminates the need for collateral.
Flash loans have two faces from a security perspective. They are a legitimate primitive that powers arbitrage, debt refinancing, collateral swapping, and other beneficial workflows. They are also the dominant attack amplifier in DeFi exploits: Section 3.10.3 (bZx), Section 3.10.6 (Nomad in some attacker patterns), and Section 3.10.8 (Euler) all involved flash loans as the capital that turned a small per-dollar bug into a nine-figure loss. The same primitive supports both uses, and a protocol designer must reason about both: how to safely offer flash loans, and how to defend against attackers who use flash loans against the protocol.
This subsection covers the architectural design problem. The historical incidents are in Section 3.10. The vulnerability-class treatment is in Section 3.8.5 (oracle manipulation, often flash-loan-amplified). What follows is the developer's framing: when flash loans are appropriate, what threat model they impose, and how to design protocols that are robust under that threat model.
What Flash Loans Are, Mechanically
A flash loan is a single-transaction pattern with three distinct phases:
- Borrow. The lender transfers assets to the borrower (or to a borrower-specified address)
- Use. Control passes to the borrower, who can do anything with the borrowed assets within the constraints of the transaction
- Repay. Before the function returns, the borrower must have returned the principal plus fee; otherwise the entire transaction reverts
The lender's safety comes from atomicity. The EVM gives a binary outcome: either every step succeeds (including repayment) or every step is reverted as if it never happened. The lender has no credit risk because non-repayment means the loan never happened.
The mechanism enables uncollateralized borrowing because the borrower's intent (to repay) is enforceable by code rather than by trust. A traditional bank lends because it believes the borrower will repay; a flash loan lender lends because if the borrower doesn't repay, the transaction reverts and the loan is unmade.
Reference Implementations
The two dominant flash loan implementations in 2026:
Aave V3 flash loans (the historical standard):
import "@aave/core-v3/contracts/flashloan/interfaces/IFlashLoanReceiver.sol";
contract MyFlashLoanReceiver is IFlashLoanReceiver {
IPool public immutable pool;
function initiateFlashLoan(address asset, uint256 amount) external {
address[] memory assets = new address[](1);
uint256[] memory amounts = new uint256[](1);
uint256[] memory modes = new uint256[](1);
assets[0] = asset;
amounts[0] = amount;
modes[0] = 0; // no debt mode = flash loan
pool.flashLoan(
address(this), // receiver
assets,
amounts,
modes,
address(this), // onBehalfOf (unused for flash loans)
"", // params for the callback
0 // referralCode
);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external override returns (bool) {
require(msg.sender == address(pool), "not pool");
require(initiator == address(this), "wrong initiator");
// ... arbitrage, debt swap, etc.
// Approve repayment
for (uint i = 0; i < assets.length; i++) {
IERC20(assets[i]).approve(address(pool), amounts[i] + premiums[i]);
}
return true;
}
}
ERC-3156 (a standard for flash loans that several protocols implement):
import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
contract MyERC3156Borrower is IERC3156FlashBorrower {
bytes32 constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
function flashBorrow(
IERC3156FlashLender lender,
address token,
uint256 amount
) external {
lender.flashLoan(IERC3156FlashBorrower(this), token, amount, "");
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns (bytes32) {
require(initiator == address(this), "wrong initiator");
require(msg.sender == address(lender), "not lender");
// ... do work ...
IERC20(token).approve(msg.sender, amount + fee);
return CALLBACK_SUCCESS;
}
}
Aave V3 and ERC-3156 share the same structural pattern: the lender transfers assets, calls back into the borrower, then verifies repayment. The differences are in the interface details (parameter ordering, return semantics, callback identifier).
Some flash loan sources don't conform to either standard. Balancer V2's flash loans use yet another interface. Uniswap V3's flash swaps are not strictly flash loans but provide similar capabilities. The fragmentation matters because a protocol consuming flash loans must integrate with each source's specific interface.
Legitimate Uses of Flash Loans
For all the attention to flash-loan exploits, the intended uses of the primitive remain economically important. The most common:
Arbitrage
The largest single use. A searcher detects a price discrepancy between two venues, takes a flash loan, executes the arbitrage, repays the loan from the profit. The arbitrage closes the price gap; the searcher captures the closing profit minus fees and loan premium.
function executeArbitrage(
address[] calldata venues,
bytes calldata path
) external {
// 1. Take flash loan
pool.flashLoan(address(this), USDC, 1_000_000e6, /* ... */);
}
function executeOperation(...) external override returns (bool) {
// 2. Swap USDC → ETH on venue A (cheap)
uint256 ethReceived = _swapOnVenue(venueA, USDC, ETH, 1_000_000e6);
// 3. Swap ETH → USDC on venue B (expensive)
uint256 usdcReturned = _swapOnVenue(venueB, ETH, USDC, ethReceived);
// 4. Verify profit covers loan + premium
require(usdcReturned > 1_000_000e6 + premium, "unprofitable");
// 5. Repay loan
IERC20(USDC).approve(address(pool), 1_000_000e6 + premium);
return true;
}
This is benign — it improves market efficiency and is the textbook flash loan use case.
Debt Refinancing
A user has a debt position on protocol A (high interest rate, suboptimal terms). They use a flash loan to repay protocol A's debt, withdraw their collateral, open an equivalent position on protocol B (better terms), and use the new borrowing to repay the flash loan. The user has refinanced their debt atomically without ever closing their position fully.
This avoids the alternative — repaying with their own funds (which they may not have) or sequentially closing and re-opening positions (which exposes them to liquidation in between).
Collateral Swapping
A user has a borrowing position with ETH as collateral and wants to switch to BTC as collateral without closing the position. A flash loan provides temporary BTC; the user uses it to back the existing loan, swaps the ETH out, sells the ETH for BTC, returns the borrowed BTC. The user's position is preserved with new collateral.
Self-Liquidation (in some cases)
A user near liquidation can use a flash loan to repay their debt, withdraw collateral, swap collateral for the debt token, and repay the flash loan. They escape liquidation by exiting the position atomically. This is the legitimate use of self-liquidation — distinct from the Euler exploit pattern (Section 3.10.8), where the attacker manipulated the protocol's logic to extract value via self-liquidation.
Initial Liquidity Provisioning
Some protocols use flash loans to bootstrap initial liquidity for new pools, vaults, or positions without requiring the protocol team to provide working capital upfront.
The Attacker's View of Flash Loans
For an attacker, flash loans provide three things that change the threat model:
1. Effectively unlimited single-transaction capital. Major flash loan venues (Aave, Balancer, Maker, DyDx historically, Uniswap V3 via flash swaps) can collectively lend hundreds of millions of dollars in a single transaction. An attacker who can find a per-dollar profitable bug — even a small one — can amplify it to nine-figure outcomes.
2. No identity friction. Flash loans are permissionless. There is no credit check, KYC, or counterparty assessment. The attacker doesn't need a relationship with the lender; they need a contract that calls the loan function.
3. Atomic risk transfer. The attacker bears the cost of the loan fee only if their attack succeeds; otherwise the transaction reverts and they pay only gas. They can attempt arbitrarily many attacks at nearly-zero cost, simulating each off-chain until they find one that works.
Each of these changes the threat model in specific ways. The composite effect: any economic bug in a DeFi protocol is exploitable at the full capital available across flash loan venues, attempted essentially for free.
Implications for Defense
Designing under this threat model produces different decisions than designing under the implicit "honest user with their own capital" assumption.
Consider a price oracle that derives the price from an AMM's spot reserves. Under the "honest user" assumption, the price is reliable because moving it would require substantial capital. Under the flash-loan-equipped threat model, the cost of manipulation is the loan fee (typically 0.05-0.09%) plus slippage from the AMM trade itself. For an attacker who can extract more than this cost from the manipulated price, the attack is profitable.
bZx (Section 3.10.3) and Euler (Section 3.10.8) are the canonical illustrations. In both, the proximate bug was small (a wrong slippage check, a missing solvency check). The flash loan was what turned the small bug into a $100M+ loss.
Defending Against Flash-Loan-Amplified Attacks
The defenses are mostly not about flash loans themselves. Protocols cannot prevent users from taking flash loans (that's controlled by the loan source, not the consuming protocol). The defenses are about what your protocol exposes that flash-loan-equipped attackers could exploit.
Don't Use Manipulable Spot Prices
The most common flash-loan-amplified attack pattern. If your protocol reads a price from a single AMM pool, an attacker with a flash loan can move that price arbitrarily within the transaction, read the manipulated price, and exploit the resulting state. Section 3.11.1 covers oracle design in depth; the short version: use Chainlink or TWAPs as the primary price source, not spot DEX reads.
Enforce Invariants After Every State-Changing Operation
Euler's exploit (Section 3.10.8) worked because donateToReserves skipped the solvency check that every other state-changing function performed. The protocol assumed no rational user would call the function in a way that broke solvency; under the flash-loan threat model, the attacker had a way to profit from breaking it.
The defense: every state-changing function must enforce all relevant invariants, regardless of whether a rational user would benefit from violating them. The Foundry test pattern:
function test_invariant_protocolSolvency() public {
// For each public function, verify the protocol remains solvent after it
// (assert that sum(collateral) >= sum(debt) * threshold across all users)
}
Rate-Limit Per-Block State Changes
Some protocols limit how much a single user can change in a single block. The cap is set above legitimate usage but below the scale at which a flash loan could cause irrecoverable damage.
mapping(uint256 => mapping(address => uint256)) public blockChanges;
function deposit(uint256 amount) external {
blockChanges[block.number][msg.sender] += amount;
require(blockChanges[block.number][msg.sender] <= MAX_PER_BLOCK_PER_USER,
"exceeded per-block cap");
// ... actually deposit
}
This is most useful for protocols where the natural usage pattern is many small transactions rather than few large ones. For DEX-style protocols with legitimately large trades, per-block caps create friction without much defensive value.
Time-Delayed Settlement for Privileged Operations
For operations where flash-loan amplification would be catastrophic (large oracle-dependent liquidations, governance decisions, multi-million-dollar withdrawals), require a multi-block delay between request and execution.
function requestLargeWithdrawal(uint256 amount) external {
require(amount > LARGE_THRESHOLD, "use regular withdrawal");
pendingWithdrawals[msg.sender] = PendingWithdrawal({
amount: amount,
readyAt: block.timestamp + WITHDRAWAL_DELAY
});
}
function executeLargeWithdrawal() external {
PendingWithdrawal memory pending = pendingWithdrawals[msg.sender];
require(pending.amount > 0, "no pending");
require(block.timestamp >= pending.readyAt, "not ready");
delete pendingWithdrawals[msg.sender];
_transfer(msg.sender, pending.amount);
}
Flash loans can only persist for a single transaction. Multi-block delays defeat them entirely. The tradeoff: legitimate users wait. For high-value operations, this tradeoff is usually acceptable.
Block Flash-Loan-Induced Behavior
Some protocols explicitly detect when their state is being read inside a transaction that took a flash loan and behave differently. The detection is heuristic — there's no canonical "is this a flash loan?" flag — but several signals can be combined:
- High borrowing from known flash loan sources in the same transaction
- Position size that suddenly exceeds the user's historical activity
- Net-positive cash flow at transaction end that exceeds principal
This pattern is brittle and adds complexity; most protocols don't use it. It's mentioned here primarily because it's been attempted; the more robust approach is removing the underlying manipulable state.
Cooldown Between Deposit and Withdraw
For protocols where flash-loan-amplified deposit/withdraw cycles are an attack surface (e.g., share-token vaults where the price changes between deposit and withdraw), enforce a minimum holding period.
mapping(address => uint256) public lastDepositBlock;
function deposit(uint256 amount) external {
lastDepositBlock[msg.sender] = block.number;
// ...
}
function withdraw(uint256 amount) external {
require(block.number > lastDepositBlock[msg.sender], "same-block withdraw");
// ...
}
Even a one-block cooldown blocks single-transaction attacks. Multi-block cooldowns add resistance to multi-block schemes.
Offering Flash Loans Safely
For a protocol that wants to be a flash loan source — exposing flash loans on its own pooled liquidity — the design considerations are different. The protocol needs to ensure that:
- The borrowed amount is exactly the amount that was supposed to leave the contract
- The repaid amount is at least the principal plus fee
- The protocol's other state-dependent operations are not exploitable within the loan
The Pattern
contract FlashLoanProvider {
using SafeERC20 for IERC20;
IERC20 public immutable asset;
uint256 public constant FEE_BPS = 9; // 0.09%
function flashLoan(
address recipient,
uint256 amount,
bytes calldata data
) external {
require(amount > 0, "zero amount");
// Snapshot the pre-loan state
uint256 balanceBefore = asset.balanceOf(address(this));
require(balanceBefore >= amount, "insufficient liquidity");
// Compute fee
uint256 fee = (amount * FEE_BPS) / 10_000;
// Transfer principal
asset.safeTransfer(recipient, amount);
// Callback
IFlashLoanReceiver(recipient).executeOperation(
address(asset),
amount,
fee,
msg.sender,
data
);
// Verify repayment
uint256 balanceAfter = asset.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee, "loan not repaid");
// Credit the fee to the protocol's reserves
_accrueReserveFee(fee);
}
}
The structure is uniform across implementations: transfer, callback, verify. The verification uses balance comparison rather than approval-based reclaim, because balance comparison cannot be circumvented by the borrower.
Composability Considerations
The flash loan callback is, by design, a reentry into your protocol's state. The borrower can call any of your protocol's functions during the callback. The protocol must therefore:
- Reentrancy-guard the flash loan function itself (so the borrower can't take another flash loan recursively)
- Ensure that other functions on the protocol behave correctly when called during a flash loan's callback (the borrower may use the borrowed capital to perform any operation)
- Not assume balance accounting is meaningful during the callback (the protocol's balance is temporarily reduced by the loan amount)
The general principle: inside the flash loan callback, the protocol is in an intermediate state. Operations that assume the protocol's normal balance / invariants may behave incorrectly if called during this window.
Don't Allow Flash Loans of the Same Asset You Hold as Reserves
A subtle anti-pattern: if your protocol holds asset X both as user collateral and as the lendable supply for flash loans, the boundary between "user funds" and "lendable funds" must be enforced. Otherwise a borrower could effectively borrow user collateral.
The canonical fix: explicit segregation of pools. The flash-loanable pool has its own balance tracking, separate from the lending pool's collateral tracking. Modern lending protocols (Aave V3, etc.) do this explicitly.
When Not to Use Flash Loans
For consumers of flash loans, the primitive is not always the right tool. Specific cases where flash loans add risk without benefit:
1. Operations the user could fund with their own capital. A user who has enough USDC to perform an arbitrage doesn't need to flash-loan it; doing so adds the loan fee for no benefit. Flash loans are for amplifying beyond what the user could do directly.
2. Operations where the flash loan source's solvency matters. Flash loans are atomic with the borrower's transaction, but the source's solvency must be maintained for the loan to succeed. If your strategy depends on a specific flash loan being available, you're depending on the source contract not being paused, drained, or otherwise unavailable.
3. Operations that need to span multiple blocks. Flash loans terminate at the end of the borrowing transaction. Any state that needs to persist beyond a single transaction is not a flash loan use case.
4. Operations where the per-attempt cost is meaningful. Flash loan fees compound over many attempts. For a strategy that needs to be tried thousands of times across many blocks, the cumulative fee cost matters. Cheaper alternatives (just-in-time borrowing from a credit line) may be appropriate.
Practical Checklist
For a protocol that offers flash loans:
- Flash loan function is reentrancy-guarded (no recursive flash loans on same asset)
- Verification uses balance comparison, not allowance-based reclaim
- Fee is non-zero (zero fees create no incentive against denial-of-service attempts)
- Reserve and lendable pools are segregated (where the protocol also holds user funds)
- Tests cover the borrower reverting, returning excess, returning the wrong asset, and re-entering the protocol during callback
For a protocol that consumes flash loans:
- Each flash loan source's interface is verified against its current documentation (interfaces vary across providers)
-
The callback validates
msg.senderis the expected lender -
The callback validates
initiator(where applicable) is the protocol itself - Approval / repayment uses the exact required amount, with fee included
- Failure modes (callback reverts, repayment fails) are explicitly tested
For a protocol whose state could be exploited via flash loans:
- Spot price oracles are replaced with TWAPs, aggregated feeds, or off-chain attestations
- Every state-changing function enforces all relevant invariants (no exceptions for "obviously self-harming" functions)
- Large operations have multi-block delays where flash-loan amplification would be catastrophic
- Per-block rate limits exist for protocols where natural usage is many small transactions
- Cooldowns between deposit and withdraw prevent same-transaction attacks against share-price-based vaults
- The protocol's threat model explicitly assumes flash-loan-equipped adversaries
- Tests cover attacker contracts that take flash loans before interacting with the protocol
The first checklist (offering) is the easiest to satisfy. The third (defending against attackers who use flash loans) is the most important; the protocols that lost the most in Section 3.10 are protocols that did not satisfy it.
Cross-References
- Oracle manipulation — Section 3.8.5 and Section 3.11.1 cover the patterns that flash loans most often amplify
- bZx — Section 3.10.3 covers the historical incident that established flash loans as an attack primitive
- Euler Finance — Section 3.10.8 covers a flash-loan-amplified attack on a different vulnerability class
- Composability — Section 3.11.2 covers the broader threat-modeling principles; flash loans are one concrete capital primitive in the larger composability landscape
- MEV — Section 3.11.3 covers ordering-based extraction, which flash loans frequently support
- Defensive patterns — Section 3.7.5 covers rate limits and pause mechanisms that mitigate flash-loan-amplified attacks
- Aave V3 flash loan documentation —
https://docs.aave.com/developers/guides/flash-loans - ERC-3156 — the EIP for a standardized flash loan interface:
https://eips.ethereum.org/EIPS/eip-3156