Advanced DeFi Flash Loan Strategies: Arbitrage, Liquidations & More
Master flash loan mechanics, build profitable arbitrage strategies, execute complex DeFi operations, and understand flash loan attack patterns with real code examples.
Prerequisites
- Solidity programming
- DeFi protocol knowledge
- Smart contract development
- Understanding of AMMs and lending protocols
Advanced DeFi Flash Loan Strategies: Arbitrage, Liquidations & More
Flash loans represent one of the most innovative financial primitives in decentralized finance - enabling anyone to borrow millions of dollars with zero collateral for a single transaction. Since their introduction by Aave in 2020, flash loans have facilitated over $8 billion in volume, enabling arbitrage opportunities, liquidations, collateral swaps, and unfortunately, numerous exploits totaling hundreds of millions in losses.
This comprehensive guide explores flash loan mechanics from first principles, teaches you to build profitable flash loan strategies, implements real arbitrage and liquidation bots, and provides frameworks for understanding both legitimate use cases and attack vectors. Whether you're a DeFi developer building flash loan functionality, a trader seeking arbitrage opportunities, or a security researcher analyzing flash loan attacks, this guide provides the technical depth needed to master this powerful DeFi tool.
Table of Contents
- Flash Loan Fundamentals
- Flash Loan Protocol Implementations
- Building Flash Loan Contracts
- DEX Arbitrage Strategies
- Liquidation Strategies
- Collateral Swap Operations
- Complex Multi-Step Strategies
- Flash Loan Attacks Analysis
- Gas Optimization Techniques
- Building Flash Loan Bots
- Risk Management and Safety
- Advanced Flash Loan Patterns
Flash Loan Fundamentals
Flash loans are uncollateralized loans that must be borrowed and repaid within a single atomic transaction, enabling capital-efficient DeFi operations impossible in traditional finance.
The Core Innovation
Atomic Transaction Property: Flash loans exploit blockchain atomicity - either an entire transaction succeeds or it completely reverts. This allows lending without collateral since the loan must be repaid before the transaction completes.
Traditional Loan:
1. Provide collateral
2. Receive loan
3. Use funds
4. Repay loan + interest
5. Retrieve collateral
Flash Loan (Single Transaction):
1. Borrow any amount (no collateral)
2. Execute operations with borrowed funds
3. Repay loan + fee
4. If step 3 fails, entire transaction reverts
→ Lender never loses funds
Economic Mechanics
Fee Structure:
- Aave: 0.09% (9 basis points)
- dYdX: 0% (subsidized)
- Balancer: Variable based on pool
- Uniswap V3: 0.05-1% depending on pool tier
Profitability Equation:
Profit = Revenue from Operations - Flash Loan Fee - Gas Costs
Where:
- Revenue: Arbitrage profit, liquidation bonus, etc.
- Flash Loan Fee: Loan Amount × Fee Percentage
- Gas Costs: Total gas used × Gas Price
For profitability:
Revenue > (Loan Amount × 0.0009) + Gas Costs
Example:
Loan: 1,000,000 USDC
Fee (0.09%): 900 USDC
Gas (500k gas @ 50 gwei): ~$12.50
Required revenue: > $912.50
Flash Loan Use Cases
Legitimate Applications:
- Arbitrage: Exploit price differences across DEXs
- Liquidations: Liquidate undercollateralized positions
- Collateral Swaps: Replace collateral without closing positions
- Debt Refinancing: Move debt between protocols for better rates
- Leverage: Increase position size without multiple transactions
Attack Vectors:
- Oracle Manipulation: Distort price feeds with large trades
- Governance Attacks: Acquire voting power temporarily
- Reentrancy Exploitation: Exploit contract vulnerabilities
- Economic Exploits: Manipulate protocol mechanics
Flash Loan Providers Comparison
Provider Comparison:
Aave V3:
- Max Loan: Entire reserve (hundreds of millions)
- Fee: 0.09%
- Assets: 20+ tokens
- Gas Cost: ~300k-500k gas
- Mode: ERC-3156 compliant
dYdX:
- Max Loan: Pool dependent
- Fee: 0% (!)
- Assets: ETH, USDC, DAI
- Gas Cost: ~400k-600k gas
- Mode: Custom implementation
Uniswap V2:
- Max Loan: Pool reserves
- Fee: 0.3%
- Assets: Any pair
- Gas Cost: ~250k-400k gas
- Mode: Callback mechanism
Balancer:
- Max Loan: Pool dependent
- Fee: Variable
- Assets: Multiple tokens simultaneously
- Gas Cost: ~300k-500k gas
- Mode: Custom callback
Flash Loan Protocol Implementations
Understanding how different protocols implement flash loans helps choose the right provider for specific strategies.
Aave Flash Loan Architecture
// Aave V3 Flash Loan Implementation
import {FlashLoanSimpleReceiverBase} from "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol";
contract AaveFlashLoan is FlashLoanSimpleReceiverBase {
address payable owner;
constructor(address _addressProvider)
FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider))
{
owner = payable(msg.sender);
}
/**
* Request flash loan from Aave
*/
function requestFlashLoan(address _token, uint256 _amount) public {
address receiverAddress = address(this);
address asset = _token;
uint256 amount = _amount;
bytes memory params = "";
uint16 referralCode = 0;
POOL.flashLoanSimple(
receiverAddress,
asset,
amount,
params,
referralCode
);
}
/**
* Callback function - execute operations with borrowed funds
*/
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
// Ensure callback is from Aave pool
require(
msg.sender == address(POOL),
"Caller must be POOL"
);
// This contract now has 'amount' of 'asset'
// YOUR LOGIC HERE
// Example: arbitrage, liquidation, etc.
_executeStrategy(asset, amount);
// Approve Aave to pull funds back
uint256 amountOwed = amount + premium;
IERC20(asset).approve(address(POOL), amountOwed);
return true;
}
function _executeStrategy(address asset, uint256 amount) internal {
// Implement your strategy here
// Must ensure profit covers flash loan fee + gas
}
// Withdraw profits
function withdraw(address _token) external {
require(msg.sender == owner, "Only owner");
IERC20 token = IERC20(_token);
token.transfer(owner, token.balanceOf(address(this)));
}
receive() external payable {}
}
dYdX Flash Loan Implementation
// dYdX Flash Loan (Solo Margin)
pragma solidity ^0.8.0;
import "./interfaces/ISoloMargin.sol";
contract DyDxFlashLoan {
ISoloMargin public soloMargin;
// dYdX markets
mapping(address => uint256) public tokenToMarketId;
constructor(address _soloMargin) {
soloMargin = ISoloMargin(_soloMargin);
// Set up market IDs
tokenToMarketId[WETH] = 0;
tokenToMarketId[SAI] = 1;
tokenToMarketId[USDC] = 2;
tokenToMarketId[DAI] = 3;
}
function initiateFlashLoan(
address token,
uint256 amount
) external {
// Create operations array
Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3);
// 1. Withdraw (borrow)
operations[0] = Actions.ActionArgs({
actionType: Actions.ActionType.Withdraw,
accountId: 0,
amount: Types.AssetAmount({
sign: false,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: amount
}),
primaryMarketId: tokenToMarketId[token],
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: ""
});
// 2. Call function (execute strategy)
operations[1] = Actions.ActionArgs({
actionType: Actions.ActionType.Call,
accountId: 0,
amount: Types.AssetAmount({
sign: false,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: 0
}),
primaryMarketId: 0,
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: abi.encode(token, amount)
});
// 3. Deposit (repay)
operations[2] = Actions.ActionArgs({
actionType: Actions.ActionType.Deposit,
accountId: 0,
amount: Types.AssetAmount({
sign: true,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: amount + 2 // +2 wei for rounding
}),
primaryMarketId: tokenToMarketId[token],
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: ""
});
// Create account info
Account.Info[] memory accountInfos = new Account.Info[](1);
accountInfos[0] = Account.Info({
owner: address(this),
number: 1
});
// Execute flash loan
soloMargin.operate(accountInfos, operations);
}
// Callback function called by dYdX
function callFunction(
address sender,
Account.Info memory account,
bytes memory data
) public {
require(msg.sender == address(soloMargin), "Only Solo Margin");
(address token, uint256 amount) = abi.decode(data, (address, uint256));
// Execute strategy with borrowed funds
_executeStrategy(token, amount);
// No need to explicitly repay - handled by deposit operation
}
function _executeStrategy(address token, uint256 amount) internal {
// Your strategy implementation
}
}
Uniswap V2 Flash Swap
// Uniswap V2 Flash Swap
contract UniswapFlashSwap {
IUniswapV2Router02 public immutable router;
IUniswapV2Factory public immutable factory;
constructor(address _router, address _factory) {
router = IUniswapV2Router02(_router);
factory = IUniswapV2Factory(_factory);
}
function initiateFlashSwap(
address token0,
address token1,
uint256 amount0,
uint256 amount1
) external {
address pairAddress = factory.getPair(token0, token1);
require(pairAddress != address(0), "Pair doesn't exist");
IUniswapV2Pair pair = IUniswapV2Pair(pairAddress);
// Request flash swap
// amount0Out and amount1Out are the amounts to borrow
// data parameter triggers callback
bytes memory data = abi.encode(token0, token1, amount0);
pair.swap(
amount0, // amount0Out
amount1, // amount1Out
address(this),
data // Non-empty triggers callback
);
}
// Uniswap V2 callback
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
// Verify caller is a Uniswap pair
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
address pair = factory.getPair(token0, token1);
require(msg.sender == pair, "Invalid pair");
require(sender == address(this), "Invalid sender");
// Decode data
(address tokenBorrow, , uint256 amountBorrow) = abi.decode(
data,
(address, address, uint256)
);
// Execute strategy
_executeStrategy(tokenBorrow, amountBorrow);
// Calculate repayment (0.3% fee)
uint256 fee = ((amountBorrow * 3) / 997) + 1;
uint256 amountToRepay = amountBorrow + fee;
// Repay flash swap
IERC20(tokenBorrow).transfer(pair, amountToRepay);
}
function _executeStrategy(address token, uint256 amount) internal {
// Strategy implementation
}
}
Building Flash Loan Contracts
Let's build a production-ready flash loan contract with best practices and safety mechanisms.
Complete Flash Loan Template
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
/**
* @title Advanced Flash Loan Contract
* @notice Production-ready flash loan implementation with safety features
*/
contract AdvancedFlashLoan is
FlashLoanSimpleReceiverBase,
Ownable,
ReentrancyGuard
{
// Strategy configuration
struct Strategy {
bool enabled;
uint256 minProfit;
uint256 maxLoan;
address[] allowedTokens;
}
mapping(bytes4 => Strategy) public strategies;
// Circuit breaker
bool public emergencyShutdown;
// Profit tracking
mapping(address => uint256) public totalProfits;
// Events
event FlashLoanExecuted(
address indexed asset,
uint256 amount,
uint256 profit,
bytes4 indexed strategyId
);
event EmergencyShutdown(bool status);
event ProfitWithdrawn(address indexed token, uint256 amount);
modifier notShutdown() {
require(!emergencyShutdown, "Emergency shutdown active");
_;
}
constructor(address _addressProvider)
FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider))
{}
/**
* @notice Execute flash loan with specified strategy
* @param asset Token to borrow
* @param amount Amount to borrow
* @param strategyId Which strategy to execute
* @param params Strategy-specific parameters
*/
function executeFlashLoan(
address asset,
uint256 amount,
bytes4 strategyId,
bytes calldata params
) external onlyOwner notShutdown nonReentrant {
Strategy memory strategy = strategies[strategyId];
require(strategy.enabled, "Strategy not enabled");
require(amount <= strategy.maxLoan, "Loan too large");
require(isAllowedToken(asset, strategy), "Token not allowed");
// Encode strategy parameters
bytes memory data = abi.encode(strategyId, params);
// Request flash loan from Aave
POOL.flashLoanSimple(
address(this),
asset,
amount,
data,
0 // referralCode
);
}
/**
* @notice Aave callback - executes strategy with borrowed funds
*/
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
require(msg.sender == address(POOL), "Invalid caller");
require(initiator == address(this), "Invalid initiator");
// Decode parameters
(bytes4 strategyId, bytes memory strategyParams) = abi.decode(
params,
(bytes4, bytes)
);
// Track initial balance
uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
// Execute strategy
_executeStrategy(strategyId, asset, amount, strategyParams);
// Calculate profit/loss
uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
uint256 totalRequired = amount + premium;
require(
balanceAfter >= totalRequired,
"Insufficient funds to repay"
);
uint256 profit = balanceAfter - totalRequired;
// Verify minimum profit
Strategy memory strategy = strategies[strategyId];
require(profit >= strategy.minProfit, "Profit below minimum");
// Track profit
totalProfits[asset] += profit;
// Approve repayment
IERC20(asset).approve(address(POOL), totalRequired);
emit FlashLoanExecuted(asset, amount, profit, strategyId);
return true;
}
/**
* @notice Route to appropriate strategy
*/
function _executeStrategy(
bytes4 strategyId,
address asset,
uint256 amount,
bytes memory params
) internal {
if (strategyId == bytes4(keccak256("ARBITRAGE"))) {
_executeArbitrage(asset, amount, params);
} else if (strategyId == bytes4(keccak256("LIQUIDATION"))) {
_executeLiquidation(asset, amount, params);
} else if (strategyId == bytes4(keccak256("COLLATERAL_SWAP"))) {
_executeCollateralSwap(asset, amount, params);
} else {
revert("Unknown strategy");
}
}
/**
* @notice Arbitrage strategy (to be implemented)
*/
function _executeArbitrage(
address asset,
uint256 amount,
bytes memory params
) internal virtual {
// Implement arbitrage logic
// Override in derived contracts
}
/**
* @notice Liquidation strategy (to be implemented)
*/
function _executeLiquidation(
address asset,
uint256 amount,
bytes memory params
) internal virtual {
// Implement liquidation logic
// Override in derived contracts
}
/**
* @notice Collateral swap strategy (to be implemented)
*/
function _executeCollateralSwap(
address asset,
uint256 amount,
bytes memory params
) internal virtual {
// Implement collateral swap logic
// Override in derived contracts
}
/**
* @notice Configure strategy parameters
*/
function setStrategy(
bytes4 strategyId,
bool enabled,
uint256 minProfit,
uint256 maxLoan,
address[] calldata allowedTokens
) external onlyOwner {
strategies[strategyId] = Strategy({
enabled: enabled,
minProfit: minProfit,
maxLoan: maxLoan,
allowedTokens: allowedTokens
});
}
/**
* @notice Emergency shutdown
*/
function setEmergencyShutdown(bool status) external onlyOwner {
emergencyShutdown = status;
emit EmergencyShutdown(status);
}
/**
* @notice Withdraw profits
*/
function withdrawProfits(address token) external onlyOwner {
uint256 balance = IERC20(token).balanceOf(address(this));
require(balance > 0, "No balance");
IERC20(token).transfer(owner(), balance);
emit ProfitWithdrawn(token, balance);
}
/**
* @notice Check if token is allowed for strategy
*/
function isAllowedToken(
address token,
Strategy memory strategy
) internal pure returns (bool) {
for (uint i = 0; i < strategy.allowedTokens.length; i++) {
if (strategy.allowedTokens[i] == token) {
return true;
}
}
return false;
}
receive() external payable {}
}
DEX Arbitrage Strategies
DEX arbitrage is the most common legitimate flash loan use case.
Simple Two-DEX Arbitrage
// Simple arbitrage between two DEXs
contract SimpleArbitrage is AdvancedFlashLoan {
IUniswapV2Router02 public immutable uniswapRouter;
IUniswapV2Router02 public immutable sushiswapRouter;
constructor(
address _addressProvider,
address _uniswapRouter,
address _sushiswapRouter
) AdvancedFlashLoan(_addressProvider) {
uniswapRouter = IUniswapV2Router02(_uniswapRouter);
sushiswapRouter = IUniswapV2Router02(_sushiswapRouter);
}
/**
* @notice Execute arbitrage between Uniswap and Sushiswap
* @param asset Token to arbitrage
* @param amount Flash loan amount
* @param params Encoded: (buyDex, sellDex, tokenPath, minProfit)
*/
function _executeArbitrage(
address asset,
uint256 amount,
bytes memory params
) internal override {
(
uint8 buyDex,
uint8 sellDex,
address[] memory path,
uint256 minProfit
) = abi.decode(params, (uint8, uint8, address[], uint256));
require(path[0] == asset, "Invalid path");
// Get routers
IUniswapV2Router02 buyRouter = buyDex == 0 ? uniswapRouter : sushiswapRouter;
IUniswapV2Router02 sellRouter = sellDex == 0 ? uniswapRouter : sushiswapRouter;
// Approve tokens
IERC20(asset).approve(address(buyRouter), amount);
// Buy on first DEX
uint256[] memory amounts = buyRouter.swapExactTokensForTokens(
amount,
0, // Accept any amount (we check profit later)
path,
address(this),
block.timestamp
);
uint256 boughtAmount = amounts[amounts.length - 1];
address finalToken = path[path.length - 1];
// Reverse path for selling
address[] memory reversePath = new address[](path.length);
for (uint i = 0; i < path.length; i++) {
reversePath[i] = path[path.length - 1 - i];
}
// Approve final token
IERC20(finalToken).approve(address(sellRouter), boughtAmount);
// Sell on second DEX
uint256[] memory amounts2 = sellRouter.swapExactTokensForTokens(
boughtAmount,
amount + minProfit, // Ensure minimum profit
reversePath,
address(this),
block.timestamp
);
uint256 finalAmount = amounts2[amounts2.length - 1];
// Verify profit
require(
finalAmount >= amount + minProfit,
"Insufficient profit"
);
}
/**
* @notice Calculate potential arbitrage profit (off-chain helper)
*/
function calculateArbitrageProfit(
address[] calldata path,
uint256 amountIn,
uint8 buyDex,
uint8 sellDex
) external view returns (uint256 profit, bool profitable) {
IUniswapV2Router02 buyRouter = buyDex == 0 ? uniswapRouter : sushiswapRouter;
IUniswapV2Router02 sellRouter = sellDex == 0 ? uniswapRouter : sushiswapRouter;
// Get amount out from buy DEX
uint256[] memory amounts1 = buyRouter.getAmountsOut(amountIn, path);
uint256 boughtAmount = amounts1[amounts1.length - 1];
// Reverse path
address[] memory reversePath = new address[](path.length);
for (uint i = 0; i < path.length; i++) {
reversePath[i] = path[path.length - 1 - i];
}
// Get amount out from sell DEX
uint256[] memory amounts2 = sellRouter.getAmountsOut(
boughtAmount,
reversePath
);
uint256 finalAmount = amounts2[amounts2.length - 1];
// Calculate profit (accounting for flash loan fee)
uint256 flashLoanFee = (amountIn * 9) / 10000; // 0.09%
if (finalAmount > amountIn + flashLoanFee) {
profit = finalAmount - amountIn - flashLoanFee;
profitable = true;
} else {
profit = 0;
profitable = false;
}
return (profit, profitable);
}
}
Multi-Hop Triangular Arbitrage
// Triangular arbitrage: A → B → C → A
contract TriangularArbitrage is AdvancedFlashLoan {
struct ArbitrageRoute {
address[] path1; // A → B
address[] path2; // B → C
address[] path3; // C → A
address dex1;
address dex2;
address dex3;
}
constructor(address _addressProvider)
AdvancedFlashLoan(_addressProvider)
{}
function _executeArbitrage(
address asset,
uint256 amount,
bytes memory params
) internal override {
ArbitrageRoute memory route = abi.decode(
params,
(ArbitrageRoute)
);
require(route.path1[0] == asset, "Invalid route start");
require(
route.path3[route.path3.length - 1] == asset,
"Invalid route end"
);
uint256 currentAmount = amount;
address currentToken = asset;
// Leg 1: A → B
currentAmount = _executeSwap(
route.dex1,
currentAmount,
route.path1
);
currentToken = route.path1[route.path1.length - 1];
// Leg 2: B → C
currentAmount = _executeSwap(
route.dex2,
currentAmount,
route.path2
);
currentToken = route.path2[route.path2.length - 1];
// Leg 3: C → A
currentAmount = _executeSwap(
route.dex3,
currentAmount,
route.path3
);
// Verify we have more than we started with
require(currentAmount > amount, "No profit");
}
function _executeSwap(
address router,
uint256 amountIn,
address[] memory path
) internal returns (uint256 amountOut) {
IERC20(path[0]).approve(router, amountIn);
uint256[] memory amounts = IUniswapV2Router02(router)
.swapExactTokensForTokens(
amountIn,
0,
path,
address(this),
block.timestamp
);
return amounts[amounts.length - 1];
}
/**
* @notice Find profitable triangular arbitrage opportunities
* @dev Called off-chain to identify opportunities
*/
function findTriangularOpportunity(
address tokenA,
address tokenB,
address tokenC,
uint256 startAmount,
address[] memory dexRouters
) external view returns (
bool profitable,
uint256 profit,
ArbitrageRoute memory route
) {
// Try all permutations of DEXs
for (uint i = 0; i < dexRouters.length; i++) {
for (uint j = 0; j < dexRouters.length; j++) {
for (uint k = 0; k < dexRouters.length; k++) {
// Calculate route: A → B → C → A
route = ArbitrageRoute({
path1: _createPath(tokenA, tokenB),
path2: _createPath(tokenB, tokenC),
path3: _createPath(tokenC, tokenA),
dex1: dexRouters[i],
dex2: dexRouters[j],
dex3: dexRouters[k]
});
uint256 finalAmount = _simulateRoute(
route,
startAmount
);
if (finalAmount > startAmount) {
profit = finalAmount - startAmount;
// Account for flash loan fee and gas
uint256 flashLoanFee = (startAmount * 9) / 10000;
uint256 estimatedGas = 800000 * tx.gasprice;
if (profit > flashLoanFee + estimatedGas) {
return (true, profit, route);
}
}
}
}
}
return (false, 0, route);
}
function _createPath(
address tokenA,
address tokenB
) internal pure returns (address[] memory) {
address[] memory path = new address[](2);
path[0] = tokenA;
path[1] = tokenB;
return path;
}
function _simulateRoute(
ArbitrageRoute memory route,
uint256 startAmount
) internal view returns (uint256) {
uint256 amount = startAmount;
// Simulate leg 1
uint256[] memory amounts1 = IUniswapV2Router02(route.dex1)
.getAmountsOut(amount, route.path1);
amount = amounts1[amounts1.length - 1];
// Simulate leg 2
uint256[] memory amounts2 = IUniswapV2Router02(route.dex2)
.getAmountsOut(amount, route.path2);
amount = amounts2[amounts2.length - 1];
// Simulate leg 3
uint256[] memory amounts3 = IUniswapV2Router02(route.dex3)
.getAmountsOut(amount, route.path3);
amount = amounts3[amounts3.length - 1];
return amount;
}
}
Liquidation Strategies
Flash loans enable atomic liquidations without requiring upfront capital.
Aave Liquidation Strategy
// Aave V3 Liquidation with Flash Loan
contract AaveLiquidator is AdvancedFlashLoan {
IPool public immutable aavePool;
IUniswapV2Router02 public immutable swapRouter;
struct LiquidationParams {
address user;
address collateralAsset;
address debtAsset;
uint256 debtToCover;
bool receiveAToken;
}
constructor(
address _addressProvider,
address _aavePool,
address _swapRouter
) AdvancedFlashLoan(_addressProvider) {
aavePool = IPool(_aavePool);
swapRouter = IUniswapV2Router02(_swapRouter);
}
function _executeLiquidation(
address asset,
uint256 amount,
bytes memory params
) internal override {
LiquidationParams memory liq = abi.decode(
params,
(LiquidationParams)
);
require(asset == liq.debtAsset, "Asset mismatch");
require(amount >= liq.debtToCover, "Insufficient loan");
// 1. Approve Aave to take borrowed tokens
IERC20(liq.debtAsset).approve(address(aavePool), liq.debtToCover);
// 2. Execute liquidation
aavePool.liquidationCall(
liq.collateralAsset, // Collateral to seize
liq.debtAsset, // Debt to repay
liq.user, // User to liquidate
liq.debtToCover, // Amount of debt to cover
liq.receiveAToken // Receive aToken or underlying
);
// 3. We now have collateral (plus liquidation bonus, typically 5-10%)
uint256 collateralReceived = IERC20(liq.collateralAsset)
.balanceOf(address(this));
// 4. Swap collateral back to debt asset
if (liq.collateralAsset != liq.debtAsset) {
_swapCollateralToDebt(
liq.collateralAsset,
liq.debtAsset,
collateralReceived
);
}
// 5. Verify we have profit after repaying flash loan
uint256 finalBalance = IERC20(liq.debtAsset).balanceOf(address(this));
require(
finalBalance > amount,
"Liquidation not profitable"
);
}
function _swapCollateralToDebt(
address collateralAsset,
address debtAsset,
uint256 amount
) internal {
// Create swap path
address[] memory path = new address[](2);
path[0] = collateralAsset;
path[1] = debtAsset;
// Approve router
IERC20(collateralAsset).approve(address(swapRouter), amount);
// Execute swap
swapRouter.swapExactTokensForTokens(
amount,
0, // We check profitability later
path,
address(this),
block.timestamp
);
}
/**
* @notice Check if a user is liquidatable
* @dev Call this off-chain to find opportunities
*/
function isUserLiquidatable(
address user
) external view returns (
bool liquidatable,
address collateralAsset,
address debtAsset,
uint256 debtToCover,
uint256 expectedProfit
) {
// Get user account data
(
uint256 totalCollateralBase,
uint256 totalDebtBase,
uint256 availableBorrowsBase,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
) = aavePool.getUserAccountData(user);
// Health factor < 1 means liquidatable
if (healthFactor >= 1e18) {
return (false, address(0), address(0), 0, 0);
}
// Get user's reserve data to find collateral and debt
// (Implementation would iterate through all reserves)
// For brevity, assuming we know collateral and debt assets
// Calculate maximum debt that can be covered (typically 50%)
debtToCover = totalDebtBase / 2;
// Calculate expected collateral received (with bonus)
uint256 liquidationBonus = 1050; // 5% bonus (10500 basis points)
uint256 collateralValue = (debtToCover * liquidationBonus) / 10000;
// Estimate profit (simplified)
expectedProfit = collateralValue - debtToCover;
return (
true,
collateralAsset,
debtAsset,
debtToCover,
expectedProfit
);
}
/**
* @notice Calculate optimal liquidation amount
*/
function calculateOptimalLiquidation(
address user,
address collateralAsset,
address debtAsset
) external view returns (uint256 optimalAmount) {
// Get user data
(
,
uint256 totalDebtBase,
,
,
,
uint256 healthFactor
) = aavePool.getUserAccountData(user);
// Calculate amount needed to bring health factor to safe level (>1.5)
// This maximizes profit while leaving user in safe state
// Simplified calculation:
// optimalAmount = amount that brings HF to 1.5
uint256 targetHealthFactor = 15e17; // 1.5
// Complex calculation would go here
// For now, return 50% of debt (maximum allowed)
optimalAmount = totalDebtBase / 2;
return optimalAmount;
}
}
Compound Liquidation
// Compound V2 Liquidation
contract CompoundLiquidator is AdvancedFlashLoan {
IComptroller public immutable comptroller;
IUniswapV2Router02 public immutable router;
constructor(
address _addressProvider,
address _comptroller,
address _router
) AdvancedFlashLoan(_addressProvider) {
comptroller = IComptroller(_comptroller);
router = IUniswapV2Router02(_router);
}
function _executeLiquidation(
address asset,
uint256 amount,
bytes memory params
) internal override {
(
address borrower,
address cTokenCollateral,
address cTokenBorrowed,
uint256 repayAmount
) = abi.decode(params, (address, address, address, uint256));
// 1. Approve cToken to take borrowed tokens
IERC20(asset).approve(cTokenBorrowed, repayAmount);
// 2. Liquidate borrow
ICToken(cTokenBorrowed).liquidateBorrow(
borrower,
repayAmount,
cTokenCollateral
);
// 3. We now have cTokens (collateral)
uint256 cTokenBalance = IERC20(cTokenCollateral).balanceOf(
address(this)
);
// 4. Redeem cTokens for underlying
ICToken(cTokenCollateral).redeem(cTokenBalance);
// 5. Get underlying token address
address underlyingCollateral = ICToken(cTokenCollateral)
.underlying();
// 6. Swap collateral to debt token
uint256 collateralAmount = IERC20(underlyingCollateral)
.balanceOf(address(this));
if (underlyingCollateral != asset) {
_swapTokens(underlyingCollateral, asset, collateralAmount);
}
// 7. Verify profitability
require(
IERC20(asset).balanceOf(address(this)) > amount,
"Not profitable"
);
}
function _swapTokens(
address fromToken,
address toToken,
uint256 amount
) internal {
address[] memory path = new address[](2);
path[0] = fromToken;
path[1] = toToken;
IERC20(fromToken).approve(address(router), amount);
router.swapExactTokensForTokens(
amount,
0,
path,
address(this),
block.timestamp
);
}
/**
* @notice Check if Compound user is liquidatable
*/
function checkLiquidation(
address borrower,
address cTokenBorrowed,
address cTokenCollateral
) external view returns (
bool canLiquidate,
uint256 maxRepay,
uint256 expectedProfit
) {
// Get account liquidity
(
uint256 error,
uint256 liquidity,
uint256 shortfall
) = comptroller.getAccountLiquidity(borrower);
require(error == 0, "Comptroller error");
// If shortfall > 0, account is liquidatable
if (shortfall == 0) {
return (false, 0, 0);
}
// Get borrowed amount
uint256 borrowedAmount = ICToken(cTokenBorrowed).borrowBalanceStored(
borrower
);
// Max repay is 50% of borrowed amount (Compound rule)
maxRepay = borrowedAmount / 2;
// Calculate collateral seized (with incentive, typically 8%)
uint256 liquidationIncentive = 1080; // 8% (10800 basis points)
uint256 collateralValue = (maxRepay * liquidationIncentive) / 10000;
// Estimate profit
expectedProfit = collateralValue - maxRepay;
return (true, maxRepay, expectedProfit);
}
}
Collateral Swap Operations
Flash loans enable swapping collateral without closing positions.
Collateral Swap Implementation
// Swap collateral in lending position without closing
contract CollateralSwapper is AdvancedFlashLoan {
IPool public immutable aavePool;
IUniswapV2Router02 public immutable router;
struct SwapParams {
address currentCollateral;
address newCollateral;
uint256 amountToSwap;
address debtAsset;
uint256 debtAmount;
}
constructor(
address _addressProvider,
address _aavePool,
address _router
) AdvancedFlashLoan(_addressProvider) {
aavePool = IPool(_aavePool);
router = IUniswapV2Router02(_router);
}
function _executeCollateralSwap(
address asset,
uint256 amount,
bytes memory params
) internal override {
SwapParams memory swap = abi.decode(params, (SwapParams));
require(asset == swap.debtAsset, "Wrong flash loan asset");
// 1. Repay debt
IERC20(swap.debtAsset).approve(address(aavePool), swap.debtAmount);
aavePool.repay(
swap.debtAsset,
swap.debtAmount,
2, // Variable rate mode
address(this)
);
// 2. Withdraw collateral
aavePool.withdraw(
swap.currentCollateral,
swap.amountToSwap,
address(this)
);
// 3. Swap old collateral for new collateral
uint256 newCollateralAmount = _swapTokens(
swap.currentCollateral,
swap.newCollateral,
swap.amountToSwap
);
// 4. Deposit new collateral
IERC20(swap.newCollateral).approve(
address(aavePool),
newCollateralAmount
);
aavePool.supply(
swap.newCollateral,
newCollateralAmount,
address(this),
0
);
// 5. Borrow to repay flash loan
aavePool.borrow(
swap.debtAsset,
amount, // Borrow exact flash loan amount
2, // Variable rate
0,
address(this)
);
// Position now has new collateral type with same debt
}
function _swapTokens(
address fromToken,
address toToken,
uint256 amountIn
) internal returns (uint256 amountOut) {
address[] memory path;
// Direct swap or through WETH
if (fromToken == WETH || toToken == WETH) {
path = new address[](2);
path[0] = fromToken;
path[1] = toToken;
} else {
path = new address[](3);
path[0] = fromToken;
path[1] = WETH;
path[2] = toToken;
}
IERC20(fromToken).approve(address(router), amountIn);
uint256[] memory amounts = router.swapExactTokensForTokens(
amountIn,
0,
path,
address(this),
block.timestamp
);
return amounts[amounts.length - 1];
}
/**
* @notice Calculate collateral swap feasibility
*/
function calculateSwap(
address user,
address currentCollateral,
address newCollateral,
uint256 amountToSwap
) external view returns (
bool feasible,
uint256 newCollateralAmount,
uint256 newHealthFactor
) {
// Get current user data
(
uint256 totalCollateralBase,
uint256 totalDebtBase,
,
,
,
uint256 currentHealthFactor
) = aavePool.getUserAccountData(user);
// Simulate swap
// Get exchange rate
address[] memory path = new address[](2);
path[0] = currentCollateral;
path[1] = newCollateral;
uint256[] memory amounts = router.getAmountsOut(amountToSwap, path);
newCollateralAmount = amounts[1];
// Calculate new health factor
// (Simplified - actual calculation more complex)
uint256 collateralValueRemoved = amountToSwap; // In base currency
uint256 collateralValueAdded = newCollateralAmount; // In base currency
uint256 newTotalCollateral = totalCollateralBase -
collateralValueRemoved +
collateralValueAdded;
newHealthFactor = (newTotalCollateral * 1e18) / totalDebtBase;
// Must maintain health factor > 1
feasible = newHealthFactor > 1e18;
return (feasible, newCollateralAmount, newHealthFactor);
}
}
Complex Multi-Step Strategies
Combining multiple DeFi operations in a single flash loan transaction.
Leveraged Yield Farming
// Open leveraged yield farming position with flash loan
contract LeveragedYieldFarming is AdvancedFlashLoan {
IPool public immutable aavePool;
IUniswapV2Router02 public immutable router;
IYearnVault public immutable yearnVault;
struct LeverageParams {
address supplyAsset;
address borrowAsset;
uint256 initialDeposit;
uint256 leverage; // In basis points (e.g., 30000 = 3x)
}
constructor(
address _addressProvider,
address _aavePool,
address _router,
address _yearnVault
) AdvancedFlashLoan(_addressProvider) {
aavePool = IPool(_aavePool);
router = IUniswapV2Router02(_router);
yearnVault = IYearnVault(_yearnVault);
}
/**
* @notice Open leveraged position
* Flow:
* 1. Flash loan borrowAsset
* 2. Swap to supplyAsset
* 3. Deposit to Aave as collateral
* 4. Borrow to repay flash loan
* 5. Remaining collateral earning yield
*/
function openLeveragedPosition(
address supplyAsset,
address borrowAsset,
uint256 initialDeposit,
uint256 leverage
) external {
// Calculate flash loan amount
uint256 flashLoanAmount = (initialDeposit * leverage) / 10000;
// Transfer initial deposit from user
IERC20(supplyAsset).transferFrom(
msg.sender,
address(this),
initialDeposit
);
// Encode parameters
bytes memory params = abi.encode(
LeverageParams({
supplyAsset: supplyAsset,
borrowAsset: borrowAsset,
initialDeposit: initialDeposit,
leverage: leverage
})
);
// Execute flash loan
POOL.flashLoanSimple(
address(this),
borrowAsset,
flashLoanAmount,
params,
0
);
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
require(msg.sender == address(POOL), "Invalid caller");
LeverageParams memory lev = abi.decode(params, (LeverageParams));
// 1. Swap borrowed asset to supply asset
uint256 supplyAmount = _swapAssets(
lev.borrowAsset,
lev.supplyAsset,
amount
);
// Add initial deposit
supplyAmount += lev.initialDeposit;
// 2. Deposit to Yearn for yield
IERC20(lev.supplyAsset).approve(address(yearnVault), supplyAmount);
yearnVault.deposit(supplyAmount);
// 3. Deposit Yearn shares to Aave as collateral
uint256 shares = IERC20(address(yearnVault)).balanceOf(address(this));
IERC20(address(yearnVault)).approve(address(aavePool), shares);
aavePool.supply(address(yearnVault), shares, address(this), 0);
// 4. Borrow to repay flash loan
uint256 totalDebt = amount + premium;
aavePool.borrow(
lev.borrowAsset,
totalDebt,
2, // Variable rate
0,
address(this)
);
// 5. Approve repayment
IERC20(asset).approve(address(POOL), totalDebt);
return true;
}
function _swapAssets(
address fromAsset,
address toAsset,
uint256 amount
) internal returns (uint256) {
address[] memory path = new address[](2);
path[0] = fromAsset;
path[1] = toAsset;
IERC20(fromAsset).approve(address(router), amount);
uint256[] memory amounts = router.swapExactTokensForTokens(
amount,
0,
path,
address(this),
block.timestamp
);
return amounts[1];
}
}
Flash Loan Attacks Analysis
Understanding attack patterns helps build defenses and identify vulnerabilities.
Price Oracle Manipulation Attack
/**
* @title Price Oracle Manipulation Example
* @notice EDUCATIONAL ONLY - Shows how flash loans can manipulate prices
*/
contract OracleManipulationExample {
// VULNERABLE PROTOCOL
IUniswapV2Pair public pricePair;
function getPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = pricePair.getReserves();
// VULNERABLE: Using spot price
return (reserve1 * 1e18) / reserve0;
}
function liquidateBasedOnPrice(address user) external {
uint256 price = getPrice();
// Use price for liquidation logic
// VULNERABLE to flash loan price manipulation
}
}
/**
* @title Price Manipulation Attack
* @notice How an attacker exploits the vulnerability
*/
contract PriceManipulationAttack {
IUniswapV2Router02 public router;
IVulnerableProtocol public target;
function executeAttack() external {
// 1. Flash loan large amount of token0
uint256 loanAmount = 1000000 * 1e18;
// In flash loan callback:
// 2. Buy massive amount of token1, skewing price
// 3. Target protocol reads manipulated price
// 4. Execute favorable action (liquidation, etc.)
// 5. Reverse trade
// 6. Repay flash loan
// 7. Keep profit
}
function flashLoanCallback(uint256 amount) external {
// 2. Manipulate price by large buy
_manipulatePrice(amount);
// 3. Exploit manipulated price
target.liquidateBasedOnPrice(victimUser);
// 4. Reverse price manipulation
_reverseManipulation();
// Profit extracted
}
}
/**
* @title SECURE Price Oracle
* @notice How to prevent manipulation
*/
contract SecureOracle {
IUniswapV2Pair public pair;
// TWAP state
uint256 public price0CumulativeLast;
uint32 public blockTimestampLast;
uint256 public priceAverage;
uint256 public constant PERIOD = 1 hours;
function update() external {
(
uint256 price0Cumulative,
,
uint32 blockTimestamp
) = UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed >= PERIOD) {
// Calculate TWAP
priceAverage = (price0Cumulative - price0CumulativeLast) /
timeElapsed;
price0CumulativeLast = price0Cumulative;
blockTimestampLast = blockTimestamp;
}
}
function getPrice() public view returns (uint256) {
// Return TWAP - resistant to single-block manipulation
return priceAverage;
}
}
Reentrancy Attack with Flash Loans
// VULNERABLE: Reentrancy in flash loan callback
contract VulnerableFlashLoanHandler {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
// VULNERABLE: External call before state update
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // Too late!
}
// Allow flash loans of ETH
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
// Send ETH
(bool success,) = msg.sender.call{value: amount}("");
require(success);
// Expect repayment + fee
require(
address(this).balance >= balanceBefore + fee,
"Flash loan not repaid"
);
}
}
// ATTACK: Exploit reentrancy
contract ReentrancyAttackWithFlashLoan {
VulnerableFlashLoanHandler public target;
uint256 public attackCount;
function attack() external payable {
// 1. Deposit some ETH
target.deposit{value: msg.value}();
// 2. Take flash loan
target.flashLoan(address(target).balance);
}
receive() external payable {
// Flash loan callback
if (attackCount < 10) {
attackCount++;
// Reenter withdraw
target.withdraw(msg.value);
}
// Repay flash loan on last iteration
}
}
// SECURE: Reentrancy protection
contract SecureFlashLoanHandler {
mapping(address => uint256) public balances;
bool private locked;
modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function withdraw(uint256 amount) external noReentrant {
require(balances[msg.sender] >= amount);
// Update state before external call
balances[msg.sender] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
function flashLoan(uint256 amount) external noReentrant {
uint256 balanceBefore = address(this).balance;
(bool success,) = msg.sender.call{value: amount}("");
require(success);
require(
address(this).balance >= balanceBefore + fee,
"Not repaid"
);
}
}
Gas Optimization Techniques
Gas optimization is critical for flash loan profitability.
Optimized Flash Loan Contract
// Gas-optimized flash loan arbitrage
contract OptimizedArbitrage is FlashLoanSimpleReceiverBase {
// Pack variables to save storage slots
struct Config {
address router1; // 20 bytes
address router2; // 20 bytes
uint96 minProfit; // 12 bytes (fits in same slot)
}
Config public config;
// Use immutable for deployment-time constants
address public immutable WETH;
address public immutable USDC;
// Cache frequently used values
uint256 private constant FEE_DENOMINATOR = 10000;
uint256 private constant FLASH_LOAN_FEE = 9; // 0.09%
constructor(
address _addressProvider,
address _weth,
address _usdc
) FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) {
WETH = _weth;
USDC = _usdc;
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
// Minimize external calls - cache values
Config memory cfg = config;
// Use unchecked for gas savings where overflow impossible
unchecked {
// Decode parameters efficiently
(address tokenOut, uint256 minOut) = abi.decode(
params,
(address, uint256)
);
// Execute swaps with cached addresses
uint256 amountOut = _swapTokens(
cfg.router1,
asset,
tokenOut,
amount
);
uint256 finalAmount = _swapTokens(
cfg.router2,
tokenOut,
asset,
amountOut
);
// Calculate profit without additional variables
// profit = finalAmount - (amount + premium)
require(
finalAmount >= amount + premium + cfg.minProfit,
"Insufficient profit"
);
// Single approval and return
IERC20(asset).approve(address(POOL), amount + premium);
}
return true;
}
function _swapTokens(
address router,
address tokenIn,
address tokenOut,
uint256 amountIn
) internal returns (uint256) {
// Approve exact amount (not max)
IERC20(tokenIn).approve(router, amountIn);
// Direct call instead of interface (saves gas)
(bool success, bytes memory data) = router.call(
abi.encodeWithSelector(
bytes4(keccak256("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)")),
amountIn,
0,
_getPath(tokenIn, tokenOut),
address(this),
block.timestamp
)
);
require(success, "Swap failed");
uint256[] memory amounts = abi.decode(data, (uint256[]));
return amounts[amounts.length - 1];
}
function _getPath(
address tokenIn,
address tokenOut
) internal pure returns (address[] memory) {
address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
return path;
}
}
Gas Optimization Checklist
/**
* Gas Optimization Techniques:
*
* 1. Storage Optimization
* - Pack variables into 32-byte slots
* - Use immutable for constants
* - Cache storage reads in memory
*
* 2. Computation Optimization
* - Use unchecked where safe
* - Avoid repeated calculations
* - Use bitwise operations
*
* 3. External Call Optimization
* - Minimize external calls
* - Batch operations
* - Use low-level calls sparingly
*
* 4. Logic Optimization
* - Short-circuit conditions
* - Early returns
* - Minimal variable declarations
*/
Building Flash Loan Bots
Automated bots monitor opportunities and execute flash loans.
Python Arbitrage Bot
# Flash loan arbitrage monitoring bot
from web3 import Web3
from eth_abi import encode_abi, decode_abi
import asyncio
import json
class FlashLoanArbitrageBot:
def __init__(self, web3_provider, contract_address, private_key):
self.w3 = Web3(Web3.HTTPProvider(web3_provider))
self.contract_address = contract_address
self.account = self.w3.eth.account.from_key(private_key)
# Load contract ABI
with open('FlashLoanArbitrage.json') as f:
contract_json = json.load(f)
self.contract = self.w3.eth.contract(
address=contract_address,
abi=contract_json['abi']
)
# DEX routers
self.uniswap_router = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
self.sushiswap_router = '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'
# Minimum profit threshold (in USD)
self.min_profit = 50
async def monitor_opportunities(self):
"""
Continuously monitor for arbitrage opportunities
"""
token_pairs = [
(WETH, USDC),
(WETH, DAI),
(WETH, USDT),
(WBTC, WETH)
]
while True:
for token0, token1 in token_pairs:
opportunity = await self.find_arbitrage(token0, token1)
if opportunity and opportunity['profit'] > self.min_profit:
print(f"Opportunity found: {opportunity}")
await self.execute_arbitrage(opportunity)
await asyncio.sleep(1) # Check every block
async def find_arbitrage(self, token0, token1):
"""
Find arbitrage opportunity between DEXs
"""
amounts_to_try = [
1000 * 10**18, # 1,000 tokens
5000 * 10**18, # 5,000 tokens
10000 * 10**18, # 10,000 tokens
]
best_opportunity = None
for amount in amounts_to_try:
# Get Uniswap price
uni_output = self.get_amount_out(
self.uniswap_router,
amount,
[token0, token1]
)
# Get SushiSwap price
sushi_output = self.get_amount_out(
self.sushiswap_router,
amount,
[token0, token1]
)
# Check both directions
if uni_output < sushi_output:
# Buy on Uni, sell on Sushi
profit = await self.calculate_profit(
amount,
token0,
token1,
'uni_to_sushi'
)
if profit > (best_opportunity or {}).get('profit', 0):
best_opportunity = {
'amount': amount,
'token0': token0,
'token1': token1,
'direction': 'uni_to_sushi',
'profit': profit
}
elif sushi_output < uni_output:
# Buy on Sushi, sell on Uni
profit = await self.calculate_profit(
amount,
token0,
token1,
'sushi_to_uni'
)
if profit > (best_opportunity or {}).get('profit', 0):
best_opportunity = {
'amount': amount,
'token0': token0,
'token1': token1,
'direction': 'sushi_to_uni',
'profit': profit
}
return best_opportunity
def get_amount_out(self, router_address, amount_in, path):
"""
Get output amount from DEX
"""
router = self.w3.eth.contract(
address=router_address,
abi=UNISWAP_ROUTER_ABI
)
amounts = router.functions.getAmountsOut(amount_in, path).call()
return amounts[-1]
async def calculate_profit(self, amount, token0, token1, direction):
"""
Calculate net profit including fees and gas
"""
# Simulate full arbitrage
if direction == 'uni_to_sushi':
buy_router = self.uniswap_router
sell_router = self.sushiswap_router
else:
buy_router = self.sushiswap_router
sell_router = self.uniswap_router
# Buy on first DEX
bought = self.get_amount_out(buy_router, amount, [token0, token1])
# Sell on second DEX
final = self.get_amount_out(sell_router, bought, [token1, token0])
# Calculate fees
flash_loan_fee = amount * 0.0009 # 0.09%
gas_cost = await self.estimate_gas_cost()
# Net profit
profit = final - amount - flash_loan_fee - gas_cost
return profit
async def estimate_gas_cost(self):
"""
Estimate gas cost in token terms
"""
gas_price = self.w3.eth.gas_price
estimated_gas = 500000 # Typical flash loan arbitrage
eth_cost = gas_price * estimated_gas
# Convert to USD or token equivalent
return eth_cost
async def execute_arbitrage(self, opportunity):
"""
Execute flash loan arbitrage
"""
try:
# Prepare transaction
if opportunity['direction'] == 'uni_to_sushi':
buy_dex = 0 # Uniswap
sell_dex = 1 # Sushiswap
else:
buy_dex = 1
sell_dex = 0
path = [opportunity['token0'], opportunity['token1']]
# Encode parameters
params = encode_abi(
['uint8', 'uint8', 'address[]', 'uint256'],
[buy_dex, sell_dex, path, int(opportunity['profit'] * 0.8)]
)
# Build transaction
strategy_id = self.w3.keccak(text="ARBITRAGE")[:4]
tx = self.contract.functions.executeFlashLoan(
opportunity['token0'],
opportunity['amount'],
strategy_id,
params
).buildTransaction({
'from': self.account.address,
'gas': 800000,
'gasPrice': self.w3.eth.gas_price,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
# Sign and send
signed_tx = self.account.sign_transaction(tx)
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"Transaction sent: {tx_hash.hex()}")
# Wait for confirmation
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt['status'] == 1:
print(f"Arbitrage successful! Profit: ${opportunity['profit']:.2f}")
else:
print("Transaction failed")
except Exception as e:
print(f"Error executing arbitrage: {e}")
# Run bot
if __name__ == "__main__":
bot = FlashLoanArbitrageBot(
web3_provider=os.getenv('WEB3_PROVIDER'),
contract_address=os.getenv('CONTRACT_ADDRESS'),
private_key=os.getenv('PRIVATE_KEY')
)
asyncio.run(bot.monitor_opportunities())
Risk Management and Safety
Flash loan strategies carry significant risks that must be managed.
Risk Mitigation Strategies
// Comprehensive risk management contract
contract RiskManagedFlashLoan is AdvancedFlashLoan {
// Risk parameters
struct RiskConfig {
uint256 maxLoanAmount;
uint256 maxSlippage;
uint256 minProfit;
uint256 maxGasPrice;
bool requiresSimulation;
}
RiskConfig public riskConfig;
// Circuit breakers
uint256 public consecutiveFailures;
uint256 public constant MAX_CONSECUTIVE_FAILURES = 3;
// Performance tracking
struct Performance {
uint256 totalExecutions;
uint256 successfulExecutions;
uint256 totalProfit;
uint256 totalLoss;
}
Performance public performance;
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
require(msg.sender == address(POOL), "Invalid caller");
// Risk checks
require(amount <= riskConfig.maxLoanAmount, "Loan too large");
require(tx.gasprice <= riskConfig.maxGasPrice, "Gas price too high");
require(
consecutiveFailures < MAX_CONSECUTIVE_FAILURES,
"Circuit breaker triggered"
);
uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
// Try to execute strategy
try this.executeStrategyWithChecks(asset, amount, params) {
uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
uint256 totalRequired = amount + premium;
if (balanceAfter >= totalRequired) {
uint256 profit = balanceAfter - totalRequired;
// Update performance metrics
performance.totalExecutions++;
performance.successfulExecutions++;
performance.totalProfit += profit;
consecutiveFailures = 0;
// Approve repayment
IERC20(asset).approve(address(POOL), totalRequired);
return true;
} else {
_handleFailure(balanceBefore - balanceAfter);
revert("Insufficient profit");
}
} catch Error(string memory reason) {
_handleFailure(balanceBefore - IERC20(asset).balanceOf(address(this)));
revert(reason);
}
}
function executeStrategyWithChecks(
address asset,
uint256 amount,
bytes calldata params
) external {
require(msg.sender == address(this), "Internal only");
// Simulate if required
if (riskConfig.requiresSimulation) {
_simulateStrategy(asset, amount, params);
}
// Execute actual strategy
_executeStrategy(bytes4(0), asset, amount, params);
}
function _handleFailure(uint256 loss) internal {
performance.totalExecutions++;
performance.totalLoss += loss;
consecutiveFailures++;
emit StrategyFailed(consecutiveFailures, loss);
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
emergencyShutdown = true;
emit EmergencyShutdown(true);
}
}
function _simulateStrategy(
address asset,
uint256 amount,
bytes memory params
) internal view {
// Simulate strategy execution
// Revert if simulation shows unprofitable
}
// Admin functions
function setRiskConfig(
uint256 maxLoanAmount,
uint256 maxSlippage,
uint256 minProfit,
uint256 maxGasPrice,
bool requiresSimulation
) external onlyOwner {
riskConfig = RiskConfig({
maxLoanAmount: maxLoanAmount,
maxSlippage: maxSlippage,
minProfit: minProfit,
maxGasPrice: maxGasPrice,
requiresSimulation: requiresSimulation
});
}
function resetCircuitBreaker() external onlyOwner {
consecutiveFailures = 0;
emergencyShutdown = false;
}
event StrategyFailed(uint256 consecutiveFailures, uint256 loss);
}
Advanced Flash Loan Patterns
Flash Minting
Some protocols offer flash minting - creating tokens from nothing for a transaction.
// MakerDAO DAI Flash Mint
interface IFlashMinter {
function flashLoan(
IFlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool);
}
contract FlashMintExample {
IFlashMinter public immutable flashMinter;
function executeFlashMint(uint256 amount) external {
bytes memory data = abi.encode(msg.sender);
flashMinter.flashLoan(
IFlashBorrower(address(this)),
DAI,
amount, // Can mint billions of DAI!
data
);
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
// Execute strategy with minted DAI
// No pool limits - can mint arbitrary amounts
// Approve repayment
IERC20(token).approve(msg.sender, amount + fee);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
Frequently Asked Questions
Are flash loans risky for borrowers?
Flash loans have no liquidation risk since they're repaid atomically. The primary risks are:
- Smart contract bugs in your strategy
- Transaction failures losing gas fees
- MEV bots front-running your strategy
- Slippage exceeding profit margins
Never deploy unaudited flash loan contracts with significant capital.
What's a realistic profit per flash loan arbitrage?
Profitable flash loan arbitrage typically yields $10-$1000 per transaction, depending on:
- Market inefficiency size
- Competition from other bots
- Flash loan amount
- Gas costs
Small traders may find it difficult to compete with well-capitalized MEV searchers.
Can flash loans fail?
Yes, flash loans fail if:
- Strategy doesn't generate enough profit to repay loan + fee
- Smart contract reverts due to bugs
- Slippage exceeds tolerance
- Another transaction front-runs and eliminates the opportunity
Failed transactions still cost gas, so simulations are critical.
Do I need millions to profit from flash loans?
No - flash loans require zero capital upfront. You only need:
- A profitable strategy
- Enough ETH for gas (typically $10-$100 per attempt)
- Technical skills to implement and deploy contracts
However, profitable opportunities are highly competitive.
Are flash loan attacks illegal?
Legal status varies by jurisdiction. Generally:
- Exploiting smart contract bugs: Legally ambiguous, potentially illegal
- Price manipulation: Likely illegal in many jurisdictions
- Pure arbitrage: Generally legal
Consult legal counsel before executing flash loan strategies.
How can protocols defend against flash loan attacks?
Defenses include:
- Time-weighted average price (TWAP) oracles
- Transaction volume limits
- Multi-block price feeds
- Flash loan resistance checks
- Economic security (stake > TVL)
No single defense is perfect - defense in depth is essential.
What's the best flash loan provider?
Depends on use case:
- Aave: Largest liquidity, most assets, standard choice
- dYdX: Zero fees, good for high-frequency strategies
- Uniswap V3: Good for specific pairs, concentrated liquidity
- Balancer: Multi-asset flash loans in one transaction
Most strategies use Aave due to deep liquidity.
Conclusion and Resources
Flash loans represent a unique DeFi primitive enabling capital-efficient strategies impossible in traditional finance. While they enable legitimate use cases like arbitrage and liquidations, they also introduce new attack vectors requiring robust defenses.
Key Takeaways
- Atomic Transactions: Flash loans leverage blockchain atomicity for uncollateralized lending
- Capital Efficiency: Enable strategies without upfront capital requirements
- High Competition: Profitable opportunities are highly competitive and automated
- Technical Complexity: Requires strong smart contract development skills
- Risk Management: Comprehensive risk controls are essential for sustainable strategies
Essential Resources
Learning:
- Aave Flash Loan Documentation
- Furucombo (no-code flash loan builder)
- Tenderly (transaction simulation)
Development:
- Hardhat/Foundry for testing
- Alchemy/Infura for RPC access
- OpenZeppelin contracts library
Security:
- Trail of Bits Security Guidelines
- Smart Contract Weakness Classification
- DeFi Security Summit talks
Flash loans will continue evolving as DeFi matures. Understanding their mechanics, risks, and opportunities is essential for anyone building or using advanced DeFi applications. Always prioritize security, simulate thoroughly, and never deploy strategies without comprehensive testing.
What's Next?
Disclaimer: This guide is for educational purposes only and should not be considered financial advice. Cryptocurrency investments carry significant risk. Always do your own research before making investment decisions.