// 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 {} }