Skip to content

DeFi Flash Loans — How They Work and a Simulated P&L Calculator

Posted on:November 11, 2024 at 10:00 AM

In traditional finance, borrowing requires collateral. Want a $10 million loan? Put up $12 million of assets first. Flash loans broke this rule entirely: borrow $100 million with zero collateral, execute any strategy you want, repay within the same transaction. If you don’t repay, the entire transaction reverts as if it never happened.

This isn’t a trick or a loophole. It’s a feature that only DeFi’s atomic transaction model makes possible.

Table of contents

Open Table of contents

The Atomic Transaction Guarantee

The key to understanding flash loans is Ethereum’s atomic execution model: either everything in a transaction succeeds, or nothing does.

When your transaction starts, the Ethereum VM takes a snapshot of the entire state. As execution proceeds, state changes are tentative. If at any point execution fails (revert, out-of-gas, assertion), the entire state rolls back to the snapshot. No partial execution.

Flash loans exploit this:

  1. Lender sends you 1,000 ETH at the start of the transaction
  2. You do whatever you want with it (trade, arbitrage, liquidate)
  3. You return 1,000 ETH + fee at the end of the transaction
  4. If step 3 fails: the entire transaction reverts, including step 1
Single Ethereum transaction (atomic — all or nothing)
════════════════════════════════════════════════════════════════════
┌─ tx starts ───────────────────────────────────────────────────────┐
│ │
│ EVM state snapshot taken ─────────────────────────────────────→ │
│ │
│ 1. flashLoan(1,000 ETH) ──→ Aave sends 1,000 ETH to you │
│ │
│ 2. Your strategy executes (inside executeOperation callback): │
│ ├─ swap ETH → token on DEX A │
│ ├─ swap token → ETH on DEX B (arbitrage profit: +5 ETH) │
│ └─ or: liquidate undercollateralised position, etc. │
│ │
│ 3. approve(Aave, 1,000.5 ETH) ← repay principal + 0.05% fee │
│ repay() ──→ Aave receives 1,000.5 ETH │
│ │
│ ✅ if all steps succeeded → state changes committed │
│ ❌ if any step reverted → entire state rolled back │
│ (Aave never lost a single wei) │
│ │
└───────────────────────────────────────────────────────────────────┘

The lender never actually risks their ETH. If they don’t get repaid, the loan never happened. The only risk is the opportunity cost of the block space — negligible compared to the utility enabled.

Aave Flash Loans: The Mechanics

Aave is the most commonly used flash loan provider. Here’s the flow:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IPool {
function flashLoan(
address receiverAddress,
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata interestRateModes, // 0 = no debt (must repay)
address onBehalfOf,
bytes calldata params,
uint16 referralCode
) external;
}
interface IFlashLoanReceiver {
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums, // fees owed
address initiator,
bytes calldata params
) external returns (bool);
}

Your contract implements IFlashLoanReceiver. Aave calls executeOperation() after transferring the borrowed assets. You have until the function returns to repay amounts[i] + premiums[i].

Aave’s fee: 0.05% of the borrowed amount.

Arbitrage Flash Loan: Full Example

Scenario: ETH/USDC on Uniswap V2 = $2,000. ETH/USDC on SushiSwap = $2,020. Spread = $20 (1%).

contract FlashArbitrage is IFlashLoanReceiver {
IPool constant AAVE_POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
IUniswapV2Router constant UNISWAP = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); // V2 Router02
IUniswapV2Router constant SUSHI = IUniswapV2Router(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function initiateArbitrage(uint256 usdcAmount) external {
address[] memory assets = new address[](1);
assets[0] = USDC;
uint256[] memory amounts = new uint256[](1);
amounts[0] = usdcAmount;
uint256[] memory modes = new uint256[](1);
modes[0] = 0; // must repay — no debt position opened
AAVE_POOL.flashLoan(address(this), assets, amounts, modes, address(this), "", 0);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata /* params */
) external override returns (bool) {
uint256 usdcBorrowed = amounts[0];
uint256 fee = premiums[0]; // 0.05% of borrowed
// Step 1: Buy ETH cheap on Uniswap V2
// Send USDC, receive ETH
uint256 ethReceived = _swapOnUniswap(USDC, WETH, usdcBorrowed);
// Step 2: Sell ETH expensive on SushiSwap
// Send ETH, receive USDC
uint256 usdcReceived = _swapOnSushi(WETH, USDC, ethReceived);
// Step 3: Repay flash loan
uint256 amountOwed = usdcBorrowed + fee;
require(usdcReceived >= amountOwed, "Not profitable after fees");
IERC20(USDC).approve(address(AAVE_POOL), amountOwed);
// Aave pulls repayment automatically
// Step 4: Keep profit
uint256 profit = usdcReceived - amountOwed;
IERC20(USDC).transfer(msg.sender, profit);
return true;
}
}

P&L Calculation

Let’s run the numbers for a $500,000 USDC flash loan with a 1% price spread:

Borrowed: $500,000 USDC
Aave fee (0.05%): $250 USDC
Step 1: Buy ETH on Uniswap
- Send: $500,000 USDC
- Price impact on $1B pool (0.05% of pool): ~0.05% impact
- Effective buy price: $2,001 (vs spot $2,000)
- ETH received: 500,000 / 2,001 = 249.875 ETH
Step 2: Sell ETH on SushiSwap
- Send: 249.875 ETH
- Price impact on SushiSwap pool: ~0.05% (assuming similar depth)
- Effective sell price: $2,018.99 (vs spot $2,020)
- USDC received: 249.875 × 2,018.99 = $504,621
P&L:
Revenue: $504,621
Aave fee: -$250
Gas cost: -$50 (rough estimate at 20 gwei)
Net profit: $504,321
Repayment: $500,250
Remaining: $4,071
ROI: $4,071 on a zero-capital trade (∞ % on capital, measured in absolute terms)

Price impact is the real constraint. As you borrow more, the price impact on both legs increases. There’s an optimal trade size that maximizes profit:

Profit(Δx) = revenue(Δx) - repayment(Δx) - gas
= [price spread × Δx - price_impact(Δx)] - [Δx + fee × Δx] - gas

For a given spread, the optimal size is approximately:

Δx_optimal ≈ spread% × pool_depth / (2 × fee_rate + price_impact_coefficient)

In practice, bots iterate numerically over possible trade sizes rather than solving analytically.

Flash Loan Attacks: The Dark Side

The same zero-collateral capability that enables arbitrage enables attacks. The biggest flash loan exploits:

bZx (February 2020) — $350,000

The first major flash loan attack:

  1. Flash borrow 10,000 ETH from dYdX
  2. Use 5,500 ETH to short ETH on bZx
  3. Use 1,300 ETH to pump ETH price on Kyber (illiquid pool)
  4. bZx’s oracle reads Kyber price → your bZx short is now “profitable”
  5. Liquidate your own bZx position → profit
  6. Repay dYdX flash loan

Total time: 1 transaction. Capital required: $0. Profit: $350,000. Oracle manipulation using a flash loan.

Cream Finance (October 2021) — $130 million

More sophisticated: recursive flash loans + reentrancy-like behavior across multiple protocols. The attacker manipulated the yUSD/yUSDC LP token price oracle, then used it as inflated collateral to borrow almost every token in Cream’s pools.

Key Lessons for Protocol Developers

  1. Never use spot prices as oracles: Use Uniswap V3 TWAP (30-minute minimum), Chainlink, or another manipulation-resistant feed
  2. Reentrancy guards aren’t enough: Flash loans let attackers enter your protocol in unexpected states. Use checks-effects-interactions religiously
  3. Same-transaction price changes are suspicious: Add circuit breakers that revert if price moves more than X% within a block

Balancer Flash Loans (0% Fee!)

Balancer offers flash loans with zero protocol fee — you only pay gas. The tradeoff: you must repay in the same token in the same transaction (stricter than Aave, which allows repaying with aTokens).

// Balancer V2 Vault flash loan
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(WETH);
uint256[] memory amounts = new uint256[](1);
amounts[0] = 1000 ether;
vault.flashLoan(
IFlashLoanRecipient(address(this)),
tokens,
amounts,
abi.encode(myData)
);

For pure arbitrage where you repay in the same token, Balancer is strictly better than Aave economically.

dYdX Flash Loans (No Fee, No Interface)

dYdX’s Solo Margin contract technically supports zero-fee flash loans, but there’s no high-level interface — you have to construct the operation sequence manually. Most bots prefer Aave or Balancer for the cleaner interface.

Building a Flash Loan Simulator

Before deploying to mainnet, test your strategy in a fork:

Terminal window
# Fork mainnet at a specific block
npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/KEY --fork-block-number 18000000
// hardhat test
const { ethers } = require("hardhat");
describe("FlashArbitrage", function () {
it("should profit from the spread", async function () {
const [deployer] = await ethers.getSigners();
const FlashArbitrage = await ethers.getContractFactory("FlashArbitrage");
const arb = await FlashArbitrage.deploy();
// Check USDC balance before
const usdc = await ethers.getContractAt("IERC20", USDC_ADDRESS);
const balanceBefore = await usdc.balanceOf(deployer.address);
// Execute flash arbitrage
await arb.initiateArbitrage(ethers.utils.parseUnits("100000", 6)); // $100k
const balanceAfter = await usdc.balanceOf(deployer.address);
const profit = balanceAfter.sub(balanceBefore);
expect(profit).to.be.gt(0);
console.log(`Profit: $${ethers.utils.formatUnits(profit, 6)}`);
});
});

Testing against a mainnet fork catches price impact calculations, oracle behavior, and gas estimates without risking real funds.

Flash loans are one of the most elegant innovations in DeFi. They make capital constraints irrelevant for on-chain strategies — if the strategy is profitable in a single atomic transaction, you can execute it with zero capital. The same property that makes them useful for arbitrage makes them dangerous when protocols make incorrect assumptions about market state. Understanding both sides is essential for anyone building or interacting with DeFi.