P17: Flash Loan Implementation
P17: Flash Loan Implementation
Project Overview
| Attribute | Value |
|---|---|
| Main Language | Solidity |
| Alternative Languages | Vyper |
| Difficulty | Expert |
| Coolness Level | Level 5: Pure Magic (Super Cool) |
| Business Potential | The “Open Core” Infrastructure |
| Knowledge Area | DeFi / Flash Loans |
| Main Book | “Mastering Ethereum” by Andreas M. Antonopoulos & Gavin Wood |
| Prerequisites | Projects 9 (ERC-20) & 10 (AMM/DEX) |
| Time Estimate | 1-2 weeks |
Learning Objectives
By completing this project, you will:
- Master transaction atomicity and understand why “all or nothing” execution enables capital-efficient borrowing without collateral
- Implement the callback pattern where the lending pool calls into the borrower contract, enabling complex multi-step operations within a single transaction
- Design arbitrage strategies that exploit price discrepancies across DEXs, understanding how flash loans democratize access to risk-free profits
- Implement robust security measures including reentrancy guards, repayment verification, and protection against malicious callback contracts
- Calculate and collect protocol fees on borrowed amounts, understanding how flash loan pools generate revenue
- Build multi-asset flash loan support enabling simultaneous borrowing of multiple tokens for complex DeFi operations
- Understand flash loan attack vectors and how protocols defend against oracle manipulation, governance attacks, and economic exploits
- Integrate with real DeFi protocols executing arbitrage across Uniswap, SushiSwap, Curve, and other AMMs
The Core Question You’re Answering
“How can you borrow millions of dollars with zero collateral, use it for profit, and return it—all in a single transaction?”
This question seems impossible in traditional finance. Banks require collateral, credit checks, and days of processing. But in DeFi, flash loans enable borrowing unlimited capital with zero upfront requirements—because the loan is atomic. If you can’t repay within the same transaction, the entire operation reverts as if it never happened.
Flash loans represent one of Web3’s most powerful primitives: they democratize access to capital. A developer with $0 to their name can execute the same arbitrage strategies as a hedge fund with billions. The blockchain doesn’t care about your credit score—only whether the transaction succeeds.
Deep Theoretical Foundation
Why Flash Loans Can Exist: Transaction Atomicity
In Ethereum (and most blockchains), transactions are atomic—they either completely succeed or completely fail. There’s no partial execution. This property, enforced at the EVM level, is what makes flash loans possible.
Traditional Loan: Flash Loan:
Day 1: Apply for loan Transaction {
Day 3: Credit check 1. Borrow $1M
Day 7: Receive funds 2. Use funds (arbitrage, etc.)
Day 30+: Repay with interest 3. Repay $1M + fee
Risk: Default If any step fails: REVERT
}
Risk: Zero (for lender)
The key insight: if repayment fails, the borrow never happened. From the lender’s perspective, there’s no credit risk—either they get repaid with fees, or the entire transaction reverts and their funds never left.
The Callback Pattern: How It Works
Flash loans use a callback pattern where the lending pool temporarily gives up control to the borrower:
Flash Loan Flow
================================================
FlashLoanPool BorrowerContract
| |
| 1. flashLoan(amount, data) |
|<---------------------------------|
| |
| 2. Transfer funds |
|--------------------------------->|
| |
| 3. executeOperation(...) |
|--------------------------------->|
| |
| (Borrower does stuff: |
| - arbitrage |
| - liquidate |
| - collateral swap) |
| |
| 4. Borrower repays |
|<---------------------------------|
| |
| 5. Verify balance >= before+fee |
| (if not, REVERT entire tx) |
| |
This is inversion of control—the pool calls a function on an untrusted contract. This is inherently dangerous (reentrancy risk), which is why proper security measures are critical.
Flash Loan Economics
Flash loans charge small fees because the risk is minimal:
| Protocol | Fee | Notes |
|---|---|---|
| Aave | 0.09% | Most popular flash loan source |
| dYdX | 0% (was) | Used internal accounting |
| Uniswap V3 | 0.05-1% | Via flash swap mechanism |
| Balancer | Variable | Pool-specific fees |
Example Arbitrage Math:
Borrow: 1,000 ETH from Aave
Fee: 0.9 ETH (0.09%)
Arbitrage opportunity:
Buy ETH on DEX_A: 1000 ETH for 3,000,000 USDC
Sell ETH on DEX_B: 1000 ETH for 3,050,000 USDC
Gross profit: 50,000 USDC
Net profit after repaying:
50,000 USDC - (0.9 ETH * 3000 USDC/ETH) = 50,000 - 2,700 = 47,300 USDC
Without flash loan (using own capital):
Need $3M upfront, same $47,300 profit
Much higher capital requirements for same return
Why Flash Loans Matter
Flash loans have several legitimate use cases:
- Arbitrage: Profit from price discrepancies across DEXs
- Collateral Swaps: Change loan collateral in one transaction
- Liquidations: Acquire funds to liquidate undercollateralized positions
- Self-Liquidation: Close your own position without selling assets first
- Governance Attacks: (concerning) Borrow governance tokens to vote
But they’ve also enabled massive exploits:
| Attack | Loss | Mechanism |
|---|---|---|
| bZx (Feb 2020) | $350K | Oracle manipulation via flash loan |
| Harvest Finance (Oct 2020) | $34M | Price manipulation attack |
| Pancake Bunny (May 2021) | $45M | Flash loan + price manipulation |
| Cream Finance (Oct 2021) | $130M | Flash loan + oracle exploit |
| Beanstalk (Apr 2022) | $182M | Flash loan governance attack |
Understanding flash loans means understanding both their power and their risks.
The Flash Loan Attack Pattern
Most flash loan exploits follow a similar pattern:
Standard Flash Loan Attack Vector
=================================
1. BORROW large amount (e.g., 100,000 ETH)
|
v
2. MANIPULATE price oracle
- Swap in AMM to move price
- Oracle reads manipulated price
|
v
3. EXPLOIT the manipulated price
- Borrow at wrong collateral ratio
- Liquidate at wrong price
- Mint tokens at wrong rate
|
v
4. REVERSE manipulation
- Swap back to restore price
- Attacker keeps profit
|
v
5. REPAY flash loan + fee
- Transaction completes
- Protocol is drained
Concepts You Must Understand First
Before implementing flash loans, ensure you understand these foundational concepts:
1. EVM Transaction Atomicity
Source: “Mastering Ethereum” Chapter 6 - Transactions
Every Ethereum transaction is atomic. When you call a function:
- All state changes happen, OR
- Everything reverts to the pre-transaction state
function atomicExample() external {
balances[msg.sender] -= 100; // Change 1
balances[recipient] += 100; // Change 2
require(someCondition); // If fails, both changes revert
}
Why it matters: Flash loans exploit atomicity—if repayment fails, the borrow never occurred.
2. The Callback Pattern in Solidity
Source: Aave V3 Documentation
The callback pattern allows one contract to call a function on another:
interface IFlashLoanReceiver {
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool);
}
Why it matters: The flash loan pool must call your contract and trust you to repay. You implement executeOperation to do your arbitrage/liquidation logic.
3. Reentrancy and Security
Source: “Mastering Ethereum” Chapter 9 - Smart Contract Security
When the pool calls your callback, you could try to re-enter the pool’s functions:
// DANGEROUS: Reentrancy attack
function executeOperation(...) external {
// Flash loan pool called us with 100 ETH
// Try to borrow again before repaying!
pool.flashLoan(100 ether, ""); // Reentrancy!
}
Why it matters: Pools must use reentrancy guards to prevent recursive borrowing.
4. DEX/AMM Mechanics
Source: Project 10 (AMM/DEX) - You should have completed this
Understanding how AMMs like Uniswap work is essential:
- Constant product formula:
x * y = k - Price impact from large swaps
- How to calculate optimal swap amounts
Why it matters: Most flash loan strategies involve DEX arbitrage.
5. Arbitrage Opportunities
Source: Real-world DeFi
Arbitrage profit exists when:
Price_DEX_A < Price_DEX_B (after accounting for fees)
Profit = (Price_B - Price_A) * Amount - GasCost - FlashFee
For profitable arbitrage:
- Price discrepancy must exceed combined fees
- Your transaction must execute before others spot it
- Price impact from your trade must not eliminate the spread
6. Gas Optimization for Complex Transactions
Flash loan arbitrage often involves:
- Multiple DEX swaps
- Token approvals
- Complex math
Every operation costs gas. Your profit must exceed gas costs.
Typical flash loan arbitrage gas:
- Flash loan overhead: ~150,000 gas
- Each DEX swap: ~100,000-200,000 gas
- Token approvals: ~45,000 gas each
- Callback overhead: ~50,000 gas
Total: 400,000-800,000 gas
At 50 gwei: 0.02-0.04 ETH ($40-80 at $2000/ETH)
7. MEV and Transaction Ordering
Source: “Flash Boys 2.0” by Daian et al.
Miners/validators can:
- See your pending transaction
- Calculate if it’s profitable
- Front-run or sandwich your trade
Why it matters: Profitable arbitrage is a competitive game. Others will try to steal your opportunity.
Questions to Guide Your Design
Pool Design Questions
- Should the pool hold ETH, ERC-20 tokens, or both?
- How do liquidity providers deposit and withdraw?
- Should LP tokens be minted to track shares?
- How do you prevent pool insolvency?
- Should there be a maximum flash loan amount?
Callback Interface Questions
- What information does the borrower need in the callback?
- How do you pass custom data to the borrower?
- Should the pool verify the borrower is a contract?
- How do you handle multiple simultaneous flash loans?
- What happens if the callback runs out of gas?
Fee Structure Questions
- What’s the optimal fee to balance usage and revenue?
- Should fees go to LPs, protocol treasury, or both?
- How do you handle fee calculation without overflow?
- Should fees be configurable by governance?
- How do competing pools affect fee economics?
Security Questions
- How do you prevent reentrancy during the callback?
- What if the borrower sends back a different token?
- How do you handle malicious callback code?
- Should flash loans be pausable in emergencies?
- How do you prevent manipulation of the pool itself?
Arbitrage Bot Questions
- How do you find profitable opportunities on-chain?
- Should you use off-chain price feeds or on-chain reserves?
- How do you calculate optimal trade amounts?
- How do you handle failed arbitrage gracefully?
- How do you compete with other MEV searchers?
Thinking Exercise: Paper Design
Before writing code, work through these scenarios on paper:
Exercise 1: Trace a Successful Arbitrage
Initial State:
- FlashLoanPool: 1000 ETH
- DEX_A (ETH/USDC): 100 ETH / 300,000 USDC (price = 3000 USDC/ETH)
- DEX_B (ETH/USDC): 100 ETH / 310,000 USDC (price = 3100 USDC/ETH)
- Arbitrage Bot: 0 ETH, 0 USDC
Questions:
1. How much ETH should the bot borrow?
2. What's the optimal trade size (considering slippage)?
3. What's the expected profit after fees?
4. Trace the exact balance changes at each step.
Work through it:
Step 1: Borrow X ETH from flash loan pool
- Pool: (1000 - X) ETH
- Bot: X ETH
Step 2: Buy USDC on DEX_B (sell ETH where ETH is expensive)
- Wait, actually sell ETH on DEX_B to get USDC at better rate
- Or… buy ETH on DEX_A where it’s cheaper, sell on DEX_B?
This gets complex. The optimal strategy depends on which DEX has the mispricing.
Exercise 2: What Happens When Arbitrage Fails?
Scenario: Bot borrows 100 ETH, but prices equalize before callback completes
1. Bot borrows 100 ETH (pool sends to bot)
2. Bot swaps 100 ETH for 300,000 USDC on DEX_A
3. Another transaction front-runs: moves DEX_B price down
4. Bot swaps 300,000 USDC for only 98 ETH on DEX_B
5. Bot tries to repay 100.09 ETH but only has 98 ETH
6. What happens?
Answer: The entire transaction reverts. Bot has 0 profit but also 0 loss. Pool never actually lost the 100 ETH—it’s as if the transaction never happened.
Exercise 3: Reentrancy Attack Vector
Malicious borrower contract:
function executeOperation(uint amount, uint fee, bytes data) external {
// Pool just sent us 100 ETH
// Pool is waiting for us to repay
// Attack: Try to borrow again before repaying!
pool.flashLoan(100 ether, "");
// Now we have 200 ETH but pool thinks it only loaned 100
// Can we repay just 100.09 and keep 100?
}
Question: How does your pool prevent this?
Answer: Use a reentrancy lock. If flashLoan is called while already in a flash loan, revert.
The Interview Questions They’ll Ask
Basic Understanding (Expect These First)
-
What is a flash loan and why can it exist in DeFi but not traditional finance?
-
Explain transaction atomicity in the EVM. How does it enable flash loans?
-
Walk me through the complete flow of a flash loan transaction, from borrow to repayment.
-
What’s the callback pattern? Why is it necessary for flash loans?
-
If a flash loan isn’t repaid, what happens to the borrowed funds?
Technical Deep Dives
-
How do you prevent reentrancy attacks in a flash loan pool?
-
A flash loan pool has 1000 ETH. User requests 1001 ETH. What should happen?
-
How do you verify that the borrower repaid the correct amount plus fee?
-
What data would you pass in the callback’s
bytes calldata dataparameter? -
How would you implement flash loans for multiple tokens simultaneously?
Arbitrage & Strategy
- Given these DEX reserves, calculate the optimal arbitrage trade:
- DEX_A: 100 ETH / 300,000 USDC
- DEX_B: 80 ETH / 256,000 USDC
- Flash loan fee: 0.09%
- Swap fee: 0.3%
-
Why might a profitable-looking arbitrage opportunity not actually be profitable?
-
How do you compete with other arbitrage bots (MEV)?
- What’s the difference between a flash swap (Uniswap) and a flash loan (Aave)?
Security & Exploits
-
Explain how flash loans enabled the bZx attack. What was the vulnerability?
-
How can protocols protect against flash loan-powered governance attacks?
-
What is oracle manipulation? How do flash loans make it easier?
-
Should flash loan pools be pausable? What are the trade-offs?
-
How would you design a protocol to be resistant to flash loan attacks?
Advanced Topics
-
How do flash loans interact with MEV (Maximal Extractable Value)?
-
Compare flash loans across Aave, dYdX, and Uniswap. What are the trade-offs?
-
Could you implement cross-chain flash loans? What are the challenges?
-
How do flash loans affect DeFi composability and systemic risk?
Hints in Layers
Layer 1: Basic Pool Structure
Start with a minimal flash loan pool for ETH:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IFlashLoanReceiver {
function executeOperation(
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bool);
}
contract FlashLoanPool {
uint256 public constant FEE_PERCENTAGE = 9; // 0.09% = 9/10000
receive() external payable {}
function poolBalance() public view returns (uint256) {
return address(this).balance;
}
function flashLoan(uint256 amount, bytes calldata data) external {
// TODO: Implement
}
}
Your task: Implement flashLoan with these steps:
- Record balance before
- Send ETH to borrower
- Call borrower’s callback
- Verify repayment
Layer 2: Complete Flash Loan Logic
function flashLoan(uint256 amount, bytes calldata data) external {
require(amount <= address(this).balance, "Insufficient pool balance");
uint256 balanceBefore = address(this).balance;
uint256 fee = (amount * FEE_PERCENTAGE) / 10000;
// Send funds to borrower
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "ETH transfer failed");
// Call borrower's callback
bool success = IFlashLoanReceiver(msg.sender).executeOperation(
amount,
fee,
data
);
require(success, "Callback failed");
// Verify repayment
require(
address(this).balance >= balanceBefore + fee,
"Flash loan not repaid"
);
emit FlashLoan(msg.sender, amount, fee);
}
What’s missing? Reentrancy protection!
Layer 3: Reentrancy Guard
contract FlashLoanPool {
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "Reentrancy detected");
_locked = 2;
_;
_locked = 1;
}
function flashLoan(uint256 amount, bytes calldata data)
external
nonReentrant // Add this!
{
// ... rest of implementation
}
}
Now recursive flashLoan calls will revert.
Layer 4: Basic Arbitrage Bot
contract ArbitrageBot is IFlashLoanReceiver {
address public pool;
address public dexA;
address public dexB;
constructor(address _pool, address _dexA, address _dexB) {
pool = _pool;
dexA = _dexA;
dexB = _dexB;
}
function executeArbitrage(uint256 amount) external {
// Initiate flash loan
IFlashLoanPool(pool).flashLoan(amount, "");
}
function executeOperation(
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bool) {
require(msg.sender == pool, "Only pool can call");
// 1. Buy tokens on DEX_A with borrowed ETH
uint256 tokensReceived = IDex(dexA).swapETHForTokens{value: amount}();
// 2. Sell tokens on DEX_B for ETH
IERC20(token).approve(dexB, tokensReceived);
uint256 ethReceived = IDex(dexB).swapTokensForETH(tokensReceived);
// 3. Repay flash loan + fee
uint256 amountOwed = amount + fee;
require(ethReceived >= amountOwed, "Arbitrage not profitable");
(bool sent, ) = pool.call{value: amountOwed}("");
require(sent, "Repayment failed");
// Profit stays in contract
return true;
}
receive() external payable {}
}
Layer 5: Multi-Asset Flash Loans
interface IMultiAssetFlashLoanReceiver {
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata fees,
address initiator,
bytes calldata params
) external returns (bool);
}
function flashLoan(
address[] calldata assets,
uint256[] calldata amounts,
bytes calldata params
) external nonReentrant {
require(assets.length == amounts.length, "Length mismatch");
uint256[] memory balancesBefore = new uint256[](assets.length);
uint256[] memory fees = new uint256[](assets.length);
// Transfer all assets to borrower
for (uint256 i = 0; i < assets.length; i++) {
balancesBefore[i] = IERC20(assets[i]).balanceOf(address(this));
fees[i] = (amounts[i] * FEE_PERCENTAGE) / 10000;
require(
balancesBefore[i] >= amounts[i],
"Insufficient balance"
);
IERC20(assets[i]).transfer(msg.sender, amounts[i]);
}
// Execute callback
require(
IMultiAssetFlashLoanReceiver(msg.sender).executeOperation(
assets,
amounts,
fees,
msg.sender,
params
),
"Callback failed"
);
// Verify all repayments
for (uint256 i = 0; i < assets.length; i++) {
require(
IERC20(assets[i]).balanceOf(address(this)) >=
balancesBefore[i] + fees[i],
"Flash loan not repaid"
);
}
}
Layer 6: Liquidity Provider Integration
contract FlashLoanPool is ERC20 {
IERC20 public immutable asset;
uint256 public constant FEE_PERCENTAGE = 9; // 0.09%
constructor(address _asset) ERC20("FlashLP", "FLP") {
asset = IERC20(_asset);
}
// LPs deposit assets and receive LP tokens
function deposit(uint256 amount) external returns (uint256 shares) {
uint256 totalAssets = asset.balanceOf(address(this));
uint256 totalShares = totalSupply();
if (totalShares == 0) {
shares = amount;
} else {
shares = (amount * totalShares) / totalAssets;
}
asset.transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, shares);
}
// LPs burn LP tokens to withdraw assets + earned fees
function withdraw(uint256 shares) external returns (uint256 amount) {
uint256 totalAssets = asset.balanceOf(address(this));
uint256 totalShares = totalSupply();
amount = (shares * totalAssets) / totalShares;
_burn(msg.sender, shares);
asset.transfer(msg.sender, amount);
}
// Flash loan fees accumulate in pool, benefiting LPs
}
Layer 7: Advanced Security
contract SecureFlashLoanPool {
// Reentrancy guard
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status = NOT_ENTERED;
// Emergency pause
bool public paused;
address public guardian;
// Maximum loan percentage of pool
uint256 public constant MAX_LOAN_PERCENTAGE = 9000; // 90%
modifier nonReentrant() {
require(_status == NOT_ENTERED, "ReentrancyGuard: reentrant call");
_status = ENTERED;
_;
_status = NOT_ENTERED;
}
modifier whenNotPaused() {
require(!paused, "Pool is paused");
_;
}
function flashLoan(uint256 amount, bytes calldata data)
external
nonReentrant
whenNotPaused
{
uint256 poolBalance = address(this).balance;
// Prevent borrowing entire pool (manipulation protection)
require(
amount <= (poolBalance * MAX_LOAN_PERCENTAGE) / 10000,
"Exceeds max loan"
);
// ... rest of implementation
}
function pause() external {
require(msg.sender == guardian, "Only guardian");
paused = true;
}
function unpause() external {
require(msg.sender == guardian, "Only guardian");
paused = false;
}
}
Layer 8: Real DEX Integration
interface IUniswapV2Router {
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external payable returns (uint[] memory amounts);
function swapExactTokensForETH(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
contract ProductionArbitrageBot is IFlashLoanReceiver {
IFlashLoanPool public pool;
IUniswapV2Router public routerA; // e.g., Uniswap
IUniswapV2Router public routerB; // e.g., SushiSwap
address public WETH;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
constructor(
address _pool,
address _routerA,
address _routerB,
address _weth
) {
pool = IFlashLoanPool(_pool);
routerA = IUniswapV2Router(_routerA);
routerB = IUniswapV2Router(_routerB);
WETH = _weth;
owner = msg.sender;
}
function executeArbitrage(
uint256 loanAmount,
address token,
uint256 minProfit
) external onlyOwner {
bytes memory data = abi.encode(token, minProfit);
pool.flashLoan(loanAmount, data);
}
function executeOperation(
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bool) {
require(msg.sender == address(pool), "Only pool");
(address token, uint256 minProfit) = abi.decode(
data,
(address, uint256)
);
// Build paths
address[] memory pathBuy = new address[](2);
pathBuy[0] = WETH;
pathBuy[1] = token;
address[] memory pathSell = new address[](2);
pathSell[0] = token;
pathSell[1] = WETH;
// Buy tokens on DEX A
uint[] memory amountsOut = routerA.swapExactETHForTokens{value: amount}(
0, // Accept any amount (calculate properly in production!)
pathBuy,
address(this),
block.timestamp + 300
);
// Approve and sell on DEX B
IERC20(token).approve(address(routerB), amountsOut[1]);
uint[] memory amountsBack = routerB.swapExactTokensForETH(
amountsOut[1],
0, // Accept any amount
pathSell,
address(this),
block.timestamp + 300
);
uint256 ethReceived = amountsBack[1];
uint256 amountOwed = amount + fee;
require(ethReceived >= amountOwed + minProfit, "Insufficient profit");
// Repay
(bool sent, ) = address(pool).call{value: amountOwed}("");
require(sent, "Repayment failed");
return true;
}
// Withdraw profits
function withdrawProfit() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {}
}
ASCII Diagrams
Flash Loan Transaction Flow
FLASH LOAN TRANSACTION
======================================================
TIME | POOL STATE | BORROWER STATE | ACTION
------|--------------|------------------|------------------
| | |
T0 | 1000 ETH | 0 ETH | Initial state
| | |
v v v
------+==============+==================+-----------------
| | | flashLoan(100)
T1 | 1000 ETH | 0 ETH | Borrow requested
| | | |
| | 100 ETH | |
| +--------->| |
T2 | 900 ETH | 100 ETH | Funds sent
| | |
| | +-------+ | executeOperation()
| | | SWAP | |
| | | 100E->| |
| | |305K $ | |
T3 | | 305K USDC | Bought on DEX_A
| | +-------+ |
| | | SWAP | |
| | |305K$->| |
| | |102 E | |
T4 | | 102 ETH | Sold on DEX_B
| | |
| | 100.09 ETH |
| <----------+ | |
T5 | 1000.09 ETH | 1.91 ETH | Repaid + fee
| | |
------+==============+==================+-----------------
| TX SUCCESS
RESULT: Pool gained 0.09 ETH fee
Borrower gained 1.91 ETH profit
All in ONE atomic transaction!
Callback Pattern Visualization
CALLBACK PATTERN
================================================
+-------------------+
| FLASH LOAN POOL |
| |
| balance: 1000 ETH |
+--------+----------+
|
1. flashLoan(100 ETH, data)
|
+--------v----------+
| Check: Can we |
| lend 100 ETH? |
+--------+----------+
| YES
|
2. Transfer 100 ETH
+--------v----------+
| BORROWER |
| CONTRACT |
+--------+----------+
|
3. executeOperation(100, 0.09, data)
|
+------------------------+------------------------+
| | |
+----v----+ +-----v-----+ +-----v-----+
| DEX A | | DEX B | | PROFIT |
| Buy | ------> | Sell | ------> | Calculate |
| Tokens | | Tokens | | |
+---------+ +-----------+ +-----+-----+
|
4. Transfer 100.09 ETH back
+--------v----------+
| FLASH LOAN POOL |
| |
| Check: balance >= |
| 1000 + 0.09? |
+--------+----------+
|
SUCCESS
Arbitrage Opportunity Detection
ARBITRAGE OPPORTUNITY DETECTION
========================================================
DEX A (Uniswap) DEX B (SushiSwap)
+-----------------------+ +-----------------------+
| ETH / USDC Pool | | ETH / USDC Pool |
| | | |
| 100 ETH | | 80 ETH |
| 300,000 USDC | | 256,000 USDC |
| | | |
| Price: 3000 USDC/ETH | | Price: 3200 USDC/ETH |
+-----------+-----------+ +-----------+-----------+
| |
| PRICE DIFFERENCE |
| $200 / ETH |
+-------------+--------------+
|
+---------v---------+
| ARBITRAGE BOT |
| |
| 1. Borrow ETH |
| 2. Buy ETH on A |
| (cheaper) |
| 3. Sell ETH on B |
| (expensive) |
| 4. Repay + profit |
+-------------------+
Calculation:
+---------------------------------------------------------+
| Borrow: 10 ETH from flash loan pool |
| Buy: 10 ETH costs ~30,300 USDC on DEX_A (with slippage) |
| Sell: 10 ETH returns ~31,800 USDC on DEX_B |
| Gross: 31,800 - 30,300 = 1,500 USDC |
| Flash fee: 10 ETH * 0.09% * 3100 = ~2.79 USDC |
| Gas: ~500,000 gas * 50 gwei = 0.025 ETH = ~77.5 USDC |
| Net Profit: 1,500 - 2.79 - 77.5 = ~1,420 USDC |
+---------------------------------------------------------+
Flash Loan Attack Vector
FLASH LOAN ORACLE MANIPULATION ATTACK
============================================================
ATTACKER VULNERABLE ORACLE
CONTRACT PROTOCOL (AMM-based)
| | |
1. Borrow 10,000 ETH | |
|--------------------------------> |
| | |
2. Swap 5,000 ETH -> USDC on AMM |
|------------------------------------------------------>
| | Price crashes! |
| | ETH: $3000 -> $2000 |
| | |
3. Use manipulated price | |
|--------------------------->| |
| "ETH collateral worth |<-----------------------|
| less, liquidate!" | Query price |
| | |
4. Liquidate positions at bad price |
|<---------------------------| |
| Receive discounted ETH | |
| | |
5. Swap USDC -> ETH (restore) |
|------------------------------------------------------>
| | Price restores |
| | |
6. Repay 10,000 ETH + fee | |
|<------------------------------- |
| | |
7. Keep profit from liquidation | |
| $$$$$$$$$ | |
RESULT: Attacker profits from manipulated liquidations
Victims lose collateral at unfair prices
Attack cost: Only flash loan fee (~0.09%)
Real World Outcome
# ==============================================================
# FLASH LOAN IMPLEMENTATION - DEPLOYMENT & TESTING
# ==============================================================
# 1. SETUP: Deploy flash loan pool with initial liquidity
# --------------------------------------------------------------
$ forge create src/FlashLoanPool.sol:FlashLoanPool \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
Deployer: 0xYourAddress
Deployed to: 0xFlashPoolAddress
Transaction hash: 0x...
# Fund the pool with 100 ETH
$ cast send $FLASH_POOL "" --value 100ether \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
# Verify pool balance
$ cast balance $FLASH_POOL --rpc-url $SEPOLIA_RPC
100000000000000000000 # 100 ETH
# 2. DEPLOY: Arbitrage bot
# --------------------------------------------------------------
$ forge create src/ArbitrageBot.sol:ArbitrageBot \
--constructor-args $FLASH_POOL $UNISWAP_ROUTER $SUSHI_ROUTER $WETH \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
Deployed to: 0xArbBotAddress
# ==============================================================
# SCENARIO A: SUCCESSFUL ARBITRAGE
# ==============================================================
$ cast send $ARB_BOT "executeArbitrage(uint256,address,uint256)" \
10ether \ # Borrow 10 ETH
$LINK_TOKEN \ # Arbitrage LINK
"100000000000000000" \ # Min profit 0.1 ETH
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
Transaction: 0xabc123...
# EXECUTION LOG:
[FlashLoanPool] flashLoan() called
Borrower: 0xArbBotAddress
Amount: 10.000000000000000000 ETH
Fee: 0.009000000000000000 ETH (0.09%)
Pool balance before: 100.000000000000000000 ETH
[ArbitrageBot] executeOperation() executing...
Received: 10.000000000000000000 ETH
[UniswapRouter] swapExactETHForTokens()
Input: 10 ETH
Output: 2,950 LINK
Path: WETH -> LINK
Price: 295 LINK/ETH
[LINK] approve(SushiRouter, 2950)
Spender: SushiSwap Router
Amount: 2,950 LINK
[SushiRouter] swapExactTokensForETH()
Input: 2,950 LINK
Output: 10.250000000000000000 ETH
Path: LINK -> WETH
Price: 287.9 LINK/ETH
[ArbitrageBot] Profit calculation:
ETH received: 10.250000000000000000
Loan + fee: 10.009000000000000000
Gross profit: 0.241000000000000000 ETH
Min required: 0.100000000000000000 ETH
CHECK PASSED
[ArbitrageBot] Repaying flash loan...
Transfer: 10.009000000000000000 ETH -> FlashLoanPool
[FlashLoanPool] Verifying repayment...
Pool balance after: 100.009000000000000000 ETH
Required minimum: 100.009000000000000000 ETH
CHECK PASSED
TRANSACTION SUCCESS!
Gas used: 485,234
Gas price: 25 gwei
Transaction cost: 0.012130850 ETH
# Final state:
Pool balance: 100.009 ETH (+0.009 ETH fee earned)
Bot balance: 0.228869150 ETH (0.241 - 0.012 gas)
# ==============================================================
# SCENARIO B: FAILED ARBITRAGE (REVERTS CLEANLY)
# ==============================================================
$ cast send $ARB_BOT "executeArbitrage(uint256,address,uint256)" \
10ether \
$LINK_TOKEN \
"1000000000000000000" \ # Min profit 1 ETH (too high!)
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
Transaction: 0xdef456...
Status: REVERTED
# EXECUTION LOG:
[FlashLoanPool] flashLoan() called
Borrower: 0xArbBotAddress
Amount: 10.000000000000000000 ETH
Fee: 0.009000000000000000 ETH
[ArbitrageBot] executeOperation() executing...
Received: 10.000000000000000000 ETH
[UniswapRouter] swapExactETHForTokens()
Input: 10 ETH
Output: 2,950 LINK
[SushiRouter] swapExactTokensForETH()
Input: 2,950 LINK
Output: 10.250000000000000000 ETH
[ArbitrageBot] Profit calculation:
ETH received: 10.250000000000000000
Loan + fee: 10.009000000000000000
Gross profit: 0.241000000000000000 ETH
Min required: 1.000000000000000000 ETH
INSUFFICIENT PROFIT - REVERTING
TRANSACTION REVERTED: "Insufficient profit"
# Result: ALL state changes undone
Pool balance: 100.009 ETH (unchanged)
Bot balance: 0.228869150 ETH (unchanged, minus gas for revert)
# ==============================================================
# SCENARIO C: MULTI-ASSET FLASH LOAN
# ==============================================================
# Deploy multi-asset pool
$ forge create src/MultiAssetFlashPool.sol:MultiAssetFlashPool \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
# Fund with multiple tokens
$ cast send $USDC "transfer(address,uint256)" $MULTI_POOL 1000000000000 \
--rpc-url $SEPOLIA_RPC --private-key $PRIVATE_KEY # 1M USDC
$ cast send $DAI "transfer(address,uint256)" $MULTI_POOL \
1000000000000000000000000 \
--rpc-url $SEPOLIA_RPC --private-key $PRIVATE_KEY # 1M DAI
$ cast send $WETH "deposit()" --value 100ether \
--rpc-url $SEPOLIA_RPC --private-key $PRIVATE_KEY
$ cast send $WETH "transfer(address,uint256)" $MULTI_POOL \
100000000000000000000 \
--rpc-url $SEPOLIA_RPC --private-key $PRIVATE_KEY # 100 WETH
# Execute multi-asset flash loan for collateral swap
$ cast send $COLLATERAL_SWAP_BOT \
"executeCollateralSwap(address[],uint256[])" \
"[$USDC,$WETH]" \
"[500000000000,50000000000000000000]" \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
# EXECUTION LOG:
[MultiAssetPool] Multi-asset flash loan initiated
Asset 0: USDC - Amount: 500,000 - Fee: 450
Asset 1: WETH - Amount: 50.0 - Fee: 0.045
[CollateralSwapBot] Executing collateral swap...
1. Repaying Aave USDC debt with borrowed USDC
2. Withdrawing ETH collateral from Aave
3. Depositing borrowed WETH as new collateral
4. Borrowing USDC against WETH
5. Repaying flash loan
[MultiAssetPool] Verifying all repayments...
USDC: 500,450 received (required: 500,450) PASS
WETH: 50.045 received (required: 50.045) PASS
TRANSACTION SUCCESS!
Result: Swapped Aave position from ETH collateral to WETH collateral
in single transaction with zero upfront capital!
# ==============================================================
# SCENARIO D: LIQUIDATION BOT
# ==============================================================
$ cast send $LIQUIDATION_BOT "executeLiquidation(address,address)" \
$UNDERCOLLATERALIZED_USER \
$USDC \
--rpc-url $SEPOLIA_RPC \
--private-key $PRIVATE_KEY
# EXECUTION LOG:
[LiquidationBot] Checking position health...
User: 0xUndercollateralizedUser
Collateral: 10 ETH ($30,000)
Debt: 25,000 USDC
Health Factor: 0.96 (< 1.0 = liquidatable!)
[FlashLoanPool] Flash loan: 12,500 USDC
[LiquidationBot] Executing liquidation...
Repaying 12,500 USDC debt (50% of position)
Receiving 4.17 ETH collateral (with 5% bonus)
[UniswapRouter] Selling 4.17 ETH...
Received: 12,510 USDC
[LiquidationBot] Repaying flash loan...
Amount: 12,511.25 USDC (including fee)
[FlashLoanPool] Repayment verified!
Profit: 12,510 - 12,511.25 = -1.25 USDC (loss due to unfavorable market)
Note: In production, bot would check profitability before executing!
# ==============================================================
# SCENARIO E: SECURITY - REENTRANCY ATTEMPT BLOCKED
# ==============================================================
$ forge test --match-test testReentrancyBlocked -vvvv
[PASS] testReentrancyBlocked()
[MaliciousBot] Initiating flash loan...
[FlashLoanPool] Entering nonReentrant
[FlashLoanPool] Transferring 10 ETH to MaliciousBot
[MaliciousBot] executeOperation called
[MaliciousBot] Attempting reentrant flashLoan()...
[FlashLoanPool] REVERT: "Reentrancy detected"
[Test] Caught expected revert - security working!
# ==============================================================
# FINAL STATE SUMMARY
# ==============================================================
Flash Loan Pool Statistics:
Total loans executed: 47
Total volume: 1,234.56 ETH
Total fees collected: 1.11 ETH
Current balance: 101.11 ETH
Utilization rate: 78%
Arbitrage Bot Statistics:
Successful arbitrages: 32
Failed (reverted): 15
Total profit: 4.82 ETH
Average profit per trade: 0.15 ETH
Win rate: 68%
Top Arbitrage Routes:
1. LINK: Uniswap -> SushiSwap (12 trades, 1.8 ETH profit)
2. UNI: SushiSwap -> Uniswap (8 trades, 1.2 ETH profit)
3. AAVE: Balancer -> Uniswap (7 trades, 0.9 ETH profit)
Complete Project Specification
Functional Requirements
- Flash Loan Pool
- Accept deposits from liquidity providers
- Issue LP tokens representing pool shares
- Execute flash loans with callback pattern
- Collect fees that accrue to LP holders
- Support both ETH and ERC-20 tokens
- Flash Loan Execution
- Transfer requested amount to borrower
- Call borrower’s callback function
- Verify repayment (principal + fee)
- Revert entire transaction if repayment fails
- Arbitrage Bot
- Execute cross-DEX arbitrage using flash loans
- Support configurable minimum profit thresholds
- Handle multiple token pairs
- Gracefully revert unprofitable trades
- Security
- Reentrancy protection on all state-changing functions
- Maximum loan limits (percentage of pool)
- Emergency pause functionality
- Access control for admin functions
- Observability
- Emit events for all flash loan operations
- Track cumulative statistics (volume, fees)
- Support off-chain monitoring
Non-Functional Requirements
- Gas Efficiency: Flash loan execution < 200,000 gas overhead
- Security: Pass security audit, resist known attack vectors
- Reliability: Handle edge cases (zero amounts, max values)
- Compatibility: Work with standard ERC-20 tokens, major DEXs
Solution Architecture
Module Structure
contracts/
├── core/
│ ├── FlashLoanPool.sol # ETH flash loan pool
│ ├── ERC20FlashLoanPool.sol # ERC-20 flash loan pool
│ └── MultiAssetFlashPool.sol # Multi-token support
├── bots/
│ ├── ArbitrageBot.sol # DEX arbitrage
│ ├── LiquidationBot.sol # Aave/Compound liquidations
│ └── CollateralSwapBot.sol # Collateral swapping
├── interfaces/
│ ├── IFlashLoanPool.sol
│ ├── IFlashLoanReceiver.sol
│ ├── IUniswapV2Router.sol
│ └── IERC20.sol
├── libraries/
│ ├── FlashLoanMath.sol # Fee calculations
│ └── ArbitrageLib.sol # Profit calculations
├── security/
│ ├── ReentrancyGuard.sol
│ └── Pausable.sol
└── test/
├── FlashLoanPool.t.sol
├── ArbitrageBot.t.sol
├── SecurityTests.t.sol
└── mocks/
├── MockDEX.sol
└── MockToken.sol
Core Interfaces
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IFlashLoanPool {
event FlashLoan(
address indexed borrower,
uint256 amount,
uint256 fee
);
function flashLoan(
uint256 amount,
bytes calldata data
) external;
function poolBalance() external view returns (uint256);
function feePercentage() external view returns (uint256);
}
interface IFlashLoanReceiver {
function executeOperation(
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bool);
}
interface IMultiAssetFlashLoanReceiver {
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata fees,
address initiator,
bytes calldata params
) external returns (bool);
}
Testing Strategy
Unit Tests
// test/FlashLoanPool.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/FlashLoanPool.sol";
contract FlashLoanPoolTest is Test {
FlashLoanPool public pool;
MockBorrower public borrower;
function setUp() public {
pool = new FlashLoanPool();
borrower = new MockBorrower(address(pool));
vm.deal(address(pool), 100 ether);
}
function test_FlashLoanSuccess() public {
vm.deal(address(borrower), 1 ether); // For fee
uint256 poolBefore = address(pool).balance;
borrower.initiateFlashLoan(10 ether);
uint256 poolAfter = address(pool).balance;
// Pool should have gained the fee
uint256 expectedFee = (10 ether * 9) / 10000;
assertEq(poolAfter, poolBefore + expectedFee);
}
function test_FlashLoanInsufficientRepayment() public {
BadBorrower bad = new BadBorrower(address(pool));
vm.expectRevert("Flash loan not repaid");
bad.initiateFlashLoan(10 ether);
}
function test_FlashLoanExceedsBalance() public {
vm.expectRevert("Insufficient pool balance");
borrower.initiateFlashLoan(1000 ether);
}
}
Fuzz Tests
function testFuzz_FlashLoan(uint256 amount) public {
// Bound to reasonable values
amount = bound(amount, 0.01 ether, 90 ether);
uint256 fee = (amount * 9) / 10000;
vm.deal(address(borrower), fee + 0.01 ether);
uint256 poolBefore = address(pool).balance;
borrower.initiateFlashLoan(amount);
assertEq(address(pool).balance, poolBefore + fee);
}
Security Tests
function test_ReentrancyBlocked() public {
ReentrantBorrower attacker = new ReentrantBorrower(address(pool));
vm.deal(address(attacker), 1 ether);
vm.expectRevert("Reentrancy detected");
attacker.attack(10 ether);
}
function test_PauseBlocksFlashLoans() public {
pool.pause();
vm.expectRevert("Pool is paused");
borrower.initiateFlashLoan(1 ether);
}
Common Pitfalls & Debugging
Pitfall 1: Forgetting Reentrancy Guard
Problem: Attacker can recursively borrow before repaying.
Solution:
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "Reentrancy detected");
_locked = 2;
_;
_locked = 1;
}
Pitfall 2: Integer Overflow in Fee Calculation
Problem: amount * feePercentage can overflow for large amounts.
Solution:
// Use mulDiv or check order of operations
uint256 fee = (amount * FEE_PERCENTAGE) / 10000;
// Or use OpenZeppelin's Math.mulDiv for safety
Pitfall 3: Not Checking Callback Return Value
Problem: Ignoring whether callback succeeded.
Solution:
bool success = IFlashLoanReceiver(msg.sender).executeOperation(...);
require(success, "Callback failed");
Pitfall 4: ETH Stuck in Borrower Contract
Problem: Borrower forgets to implement receive() or fallback().
Solution:
contract ArbitrageBot {
receive() external payable {}
// or
fallback() external payable {}
}
Pitfall 5: Unprofitable After Gas
Problem: Arbitrage looks profitable but gas costs exceed profit.
Solution:
uint256 gasStart = gasleft();
// ... do operations ...
uint256 gasUsed = gasStart - gasleft();
uint256 gasCost = gasUsed * tx.gasprice;
require(
profit > gasCost + minProfit,
"Not profitable after gas"
);
Books That Will Help
| Book | Author | Relevant Chapters | Why It Helps |
|---|---|---|---|
| Mastering Ethereum | Antonopoulos & Wood | Ch 6: Transactions, Ch 9: Security | Understand transaction atomicity and security patterns |
| How to DeFi: Advanced | CoinGecko | Ch 4: Flash Loans | Real-world flash loan use cases and economics |
| Ethereum Smart Contract Development | Marchioni | Ch 8: Security | Reentrancy and common vulnerabilities |
| Building Ethereum DApps | Infante | Ch 7: Security | Contract interaction patterns |
Additional Resources
Official Documentation
- Aave Flash Loans - The standard flash loan implementation
- Uniswap Flash Swaps - Alternative mechanism
- dYdX Flash Loans - Zero-fee flash loans (historical)
Flash Loan Attack Postmortems
- bZx Attack Analysis - First major flash loan exploit
- Harvest Finance Exploit - $34M price manipulation
- Cream Finance Hack - $130M flash loan attack
- Beanstalk Governance Attack - $182M governance takeover
MEV & Competition
- Flashbots Docs - MEV protection and searcher infrastructure
- MEV Explore - MEV dashboard and statistics
- Flash Boys 2.0 Paper - Academic analysis of DeFi MEV
Code References
Tools
- Foundry - Solidity development framework
- Tenderly - Transaction simulation and debugging
- DeFi Llama - Track TVL and find arbitrage opportunities
Self-Assessment Checklist
Before moving to the next project, verify:
- I can explain why flash loans can exist in DeFi but not traditional finance
- I understand the callback pattern and can implement it securely
- My flash loan pool correctly calculates and collects fees
- Reentrancy attacks are blocked by my implementation
- I can trace the complete flow of a flash loan transaction
- I understand why failed flash loans revert cleanly with no risk
- My arbitrage bot can execute profitable trades
- I understand how flash loans enable price manipulation attacks
- I can explain how protocols defend against flash loan exploits
- My tests cover success, failure, and security edge cases
- I understand the role of MEV in flash loan competition
- I can implement multi-asset flash loans
Extensions and Challenges
Challenge 1: Implement Flash Loan Aggregator
Build a contract that routes flash loan requests to the cheapest provider:
function flashLoan(uint256 amount, bytes calldata data) external {
// Check Aave, dYdX, Uniswap
// Route to lowest fee provider
// Handle fallback if primary fails
}
Challenge 2: Build MEV-Protected Arbitrage Bot
Integrate with Flashbots to submit bundles that can’t be front-run:
// Submit via Flashbots Protect RPC
// Include MEV share to validators
// Ensure atomic execution or nothing
Challenge 3: Implement Flash Loan Shield
Create a protocol feature that detects and blocks flash loan attacks:
modifier flashLoanShield() {
require(
!isFlashLoanContext(),
"Flash loan detected"
);
_;
}
Challenge 4: Cross-Chain Flash Loans
Design a system for flash loans across L1 and L2:
// Borrow on Ethereum
// Bridge to Arbitrum
// Execute arbitrage on Arbitrum
// Bridge back
// Repay on Ethereum
// All "atomic" via message passing
What’s Next?
With flash loans mastered, you now understand one of DeFi’s most powerful primitives. You’ve learned how transaction atomicity enables risk-free borrowing, how the callback pattern works, and how to build profitable arbitrage strategies.
In Project 18: Block Explorer, you’ll shift from writing smart contracts to reading blockchain data. You’ll build an indexer that tracks transactions, decodes function calls, and presents on-chain activity—essential infrastructure for any DeFi user or developer.