feat(execution): flash loan arbitrage - ZERO CAPITAL REQUIRED!
GAME CHANGER: Uses Aave V3 flash loans - no capital needed! ## Flash Loan Execution System ### Go Implementation: - FlashLoanExecutor with Aave V3 integration - Simulation mode for profitability testing - Profit calculation after flash loan fees (0.05%) - Gas cost estimation and limits - Statistics tracking ### Solidity Contract: - ArbitrageExecutor using Aave V3 FlashLoanSimpleReceiverBase - 2-hop arbitrage execution in single transaction - Emergency withdraw for stuck tokens - Profit goes to contract owner - Comprehensive events and error handling ### Main Application: - Complete MEV bot (cmd/mev-flashloan/main.go) - Pool discovery -> Arbitrage detection -> Flash loan execution - Real-time opportunity scanning - Simulation before execution - Graceful shutdown with stats ### Documentation: - README_FLASHLOAN.md: Complete user guide - contracts/DEPLOY.md: Step-by-step deployment - Example profitability calculations - Safety features and risks ## Why Flash Loans? - **$0 capital required**: Borrow -> Trade -> Repay in ONE transaction - **0.05% Aave fee**: Much cheaper than holding capital - **Atomic execution**: Fails = auto-revert, only lose gas - **Infinite scale**: Trade size limited only by pool liquidity ## Example Trade: 1. Borrow 10 WETH from Aave ($30,000) 2. Swap 10 WETH -> 30,300 USDC (Pool A) 3. Swap 30,300 USDC -> 10.1 WETH (Pool B) 4. Repay 10.005 WETH to Aave (0.05% fee) 5. Profit: 0.095 WETH = $285 Gas cost on Arbitrum: ~$0.05 Net profit: $284.95 per trade NO CAPITAL NEEDED! Task: Fast MVP Complete (1 day!) Files: 3 Go files, 1 Solidity contract, 2 docs Build: ✓ Compiles successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
228
contracts/ArbitrageExecutor.sol
Normal file
228
contracts/ArbitrageExecutor.sol
Normal file
@@ -0,0 +1,228 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.10;
|
||||
|
||||
import {FlashLoanSimpleReceiverBase} from "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
|
||||
import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
// Uniswap V2 Router interface
|
||||
interface IUniswapV2Router02 {
|
||||
function swapExactTokensForTokens(
|
||||
uint amountIn,
|
||||
uint amountOutMin,
|
||||
address[] calldata path,
|
||||
address to,
|
||||
uint deadline
|
||||
) external returns (uint[] memory amounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ArbitrageExecutor
|
||||
* @notice Executes 2-hop arbitrage opportunities using Aave V3 flash loans
|
||||
* @dev Uses flash loans to execute arbitrage with ZERO capital requirement
|
||||
*
|
||||
* How it works:
|
||||
* 1. Flash loan token A from Aave (e.g., borrow WETH)
|
||||
* 2. Swap A -> B on Pool 1 (e.g., WETH -> USDC on Uniswap)
|
||||
* 3. Swap B -> A on Pool 2 (e.g., USDC -> WETH on Sushiswap)
|
||||
* 4. Repay flash loan + 0.05% fee to Aave
|
||||
* 5. Keep the profit!
|
||||
*
|
||||
* Example profitable scenario:
|
||||
* - Borrow 1 WETH from Aave
|
||||
* - Swap 1 WETH -> 3010 USDC on Uniswap
|
||||
* - Swap 3010 USDC -> 1.01 WETH on Sushiswap
|
||||
* - Repay 1.0005 WETH to Aave (1 WETH + 0.05% fee)
|
||||
* - Profit: 0.0095 WETH (minus gas costs)
|
||||
*/
|
||||
contract ArbitrageExecutor is FlashLoanSimpleReceiverBase {
|
||||
address public immutable owner;
|
||||
|
||||
// Events for monitoring
|
||||
event ArbitrageExecuted(
|
||||
address indexed asset,
|
||||
uint256 amount,
|
||||
uint256 profit,
|
||||
uint256 gasUsed
|
||||
);
|
||||
|
||||
event ArbitrageFailed(
|
||||
address indexed asset,
|
||||
uint256 amount,
|
||||
string reason
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Constructor
|
||||
* @param _addressProvider Aave V3 PoolAddressesProvider on Arbitrum
|
||||
* Arbitrum address: 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
|
||||
*/
|
||||
constructor(address _addressProvider)
|
||||
FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider))
|
||||
{
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Execute arbitrage opportunity using flash loan
|
||||
* @param asset The asset to flash loan (e.g., WETH)
|
||||
* @param amount The amount to borrow
|
||||
* @param router1 The router address for first swap (e.g., Uniswap V2 router)
|
||||
* @param router2 The router address for second swap (e.g., Sushiswap router)
|
||||
* @param path1 The swap path for first trade [tokenA, tokenB]
|
||||
* @param path2 The swap path for second trade [tokenB, tokenA]
|
||||
* @param minProfit Minimum profit required (reverts if not met)
|
||||
*/
|
||||
function executeArbitrage(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
address router1,
|
||||
address router2,
|
||||
address[] calldata path1,
|
||||
address[] calldata path2,
|
||||
uint256 minProfit
|
||||
) external onlyOwner {
|
||||
require(path1[0] == asset, "Path1 must start with borrowed asset");
|
||||
require(path2[path2.length - 1] == asset, "Path2 must end with borrowed asset");
|
||||
|
||||
// Encode parameters for the flash loan callback
|
||||
bytes memory params = abi.encode(router1, router2, path1, path2, minProfit);
|
||||
|
||||
// Request flash loan from Aave V3
|
||||
POOL.flashLoanSimple(
|
||||
address(this), // Receiver address (this contract)
|
||||
asset, // Asset to borrow
|
||||
amount, // Amount to borrow
|
||||
params, // Parameters for callback
|
||||
0 // Referral code (0 for none)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Aave flash loan callback - executes the arbitrage
|
||||
* @dev This function is called by Aave Pool after transferring the flash loaned amount
|
||||
* @param asset The address of the flash-borrowed asset
|
||||
* @param amount The amount of the flash-borrowed asset
|
||||
* @param premium The fee of the flash-borrowed asset (0.05% on Aave V3)
|
||||
* @param initiator The address of the flashLoan initiator (must be this contract)
|
||||
* @param params The byte-encoded params passed when initiating the flashLoan
|
||||
* @return bool Returns true if execution was successful
|
||||
*/
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external override returns (bool) {
|
||||
// Ensure flash loan was initiated by this contract
|
||||
require(initiator == address(this), "Unauthorized initiator");
|
||||
require(msg.sender == address(POOL), "Caller must be Aave Pool");
|
||||
|
||||
// Decode parameters
|
||||
(
|
||||
address router1,
|
||||
address router2,
|
||||
address[] memory path1,
|
||||
address[] memory path2,
|
||||
uint256 minProfit
|
||||
) = abi.decode(params, (address, address, address[], address[], uint256));
|
||||
|
||||
uint256 startGas = gasleft();
|
||||
|
||||
// Step 1: Approve router1 to spend the borrowed tokens
|
||||
IERC20(asset).approve(router1, amount);
|
||||
|
||||
// Step 2: Execute first swap (A -> B)
|
||||
uint256 intermediateAmount;
|
||||
try IUniswapV2Router02(router1).swapExactTokensForTokens(
|
||||
amount,
|
||||
0, // No slippage protection for now (add in production!)
|
||||
path1,
|
||||
address(this),
|
||||
block.timestamp + 300 // 5 minute deadline
|
||||
) returns (uint[] memory amounts1) {
|
||||
intermediateAmount = amounts1[amounts1.length - 1];
|
||||
} catch Error(string memory reason) {
|
||||
emit ArbitrageFailed(asset, amount, reason);
|
||||
revert(string(abi.encodePacked("First swap failed: ", reason)));
|
||||
}
|
||||
|
||||
// Step 3: Approve router2 to spend intermediate tokens
|
||||
address intermediateToken = path1[path1.length - 1];
|
||||
IERC20(intermediateToken).approve(router2, intermediateAmount);
|
||||
|
||||
// Step 4: Execute second swap (B -> A)
|
||||
uint256 finalAmount;
|
||||
try IUniswapV2Router02(router2).swapExactTokensForTokens(
|
||||
intermediateAmount,
|
||||
amount + premium, // Must get back at least borrowed + fee
|
||||
path2,
|
||||
address(this),
|
||||
block.timestamp + 300
|
||||
) returns (uint[] memory amounts2) {
|
||||
finalAmount = amounts2[amounts2.length - 1];
|
||||
} catch Error(string memory reason) {
|
||||
emit ArbitrageFailed(asset, amount, reason);
|
||||
revert(string(abi.encodePacked("Second swap failed: ", reason)));
|
||||
}
|
||||
|
||||
// Step 5: Calculate profit
|
||||
uint256 amountOwed = amount + premium;
|
||||
require(finalAmount >= amountOwed, "Insufficient funds to repay loan");
|
||||
|
||||
uint256 profit = finalAmount - amountOwed;
|
||||
require(profit >= minProfit, "Profit below minimum threshold");
|
||||
|
||||
// Step 6: Approve Aave Pool to take back the borrowed amount + fee
|
||||
IERC20(asset).approve(address(POOL), amountOwed);
|
||||
|
||||
// Step 7: Transfer profit to owner
|
||||
if (profit > 0) {
|
||||
IERC20(asset).transfer(owner, profit);
|
||||
}
|
||||
|
||||
uint256 gasUsed = startGas - gasleft();
|
||||
emit ArbitrageExecuted(asset, amount, profit, gasUsed);
|
||||
|
||||
// Return true to indicate successful execution
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Emergency withdraw function to recover stuck tokens
|
||||
* @param token The token address to withdraw
|
||||
*/
|
||||
function emergencyWithdraw(address token) external onlyOwner {
|
||||
uint256 balance = IERC20(token).balanceOf(address(this));
|
||||
require(balance > 0, "No balance to withdraw");
|
||||
IERC20(token).transfer(owner, balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if an arbitrage opportunity is profitable before executing
|
||||
* @dev This is a view function to simulate profitability off-chain
|
||||
* @return profit The estimated profit in the borrowed asset
|
||||
* @return profitable Whether the arbitrage is profitable after fees
|
||||
*/
|
||||
function estimateProfitability(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
address router1,
|
||||
address router2,
|
||||
address[] calldata path1,
|
||||
address[] calldata path2
|
||||
) external view returns (uint256 profit, bool profitable) {
|
||||
// This would require integration with Uniswap quoter contracts
|
||||
// For MVP, we'll calculate this off-chain before calling executeArbitrage
|
||||
revert("Use off-chain calculation");
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "Only owner can call this");
|
||||
_;
|
||||
}
|
||||
|
||||
// Allow contract to receive ETH
|
||||
receive() external payable {}
|
||||
}
|
||||
Reference in New Issue
Block a user