package execution import ( "context" "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "coppertone.tech/fraktal/mev-bot/pkg/arbitrage" "coppertone.tech/fraktal/mev-bot/pkg/observability" ) // Aave V3 Pool address on Arbitrum var AaveV3PoolArbitrum = common.HexToAddress("0x794a61358D6845594F94dc1DB02A252b5b4814aD") // FlashLoanExecutor executes arbitrage opportunities using Aave V3 flash loans // This allows executing trades with ZERO capital requirement type FlashLoanExecutor struct { client *ethclient.Client logger observability.Logger aavePool common.Address executor *bind.TransactOpts // Transaction signer gasLimit uint64 maxGasPrice *big.Int // Stats executedCount uint64 profitTotal *big.Int } // Config for flash loan executor type Config struct { AavePoolAddress common.Address GasLimit uint64 MaxGasPrice *big.Int // Maximum gas price willing to pay (in wei) } // DefaultConfig returns sensible defaults for Arbitrum func DefaultConfig() Config { return Config{ AavePoolAddress: AaveV3PoolArbitrum, GasLimit: 500000, // 500k gas limit for flash loan + swaps MaxGasPrice: big.NewInt(1e9), // 1 gwei max (Arbitrum is cheap) } } // NewFlashLoanExecutor creates a new flash loan executor func NewFlashLoanExecutor( client *ethclient.Client, executor *bind.TransactOpts, logger observability.Logger, cfg Config, ) (*FlashLoanExecutor, error) { if client == nil { return nil, fmt.Errorf("client cannot be nil") } if executor == nil { return nil, fmt.Errorf("executor (transaction signer) cannot be nil") } if logger == nil { return nil, fmt.Errorf("logger cannot be nil") } return &FlashLoanExecutor{ client: client, logger: logger, aavePool: cfg.AavePoolAddress, executor: executor, gasLimit: cfg.GasLimit, maxGasPrice: cfg.MaxGasPrice, executedCount: 0, profitTotal: big.NewInt(0), }, nil } // Execute executes an arbitrage opportunity using a flash loan // Process: // 1. Flash loan the input token amount from Aave // 2. Swap through the arbitrage path (pool1 -> pool2) // 3. Repay the flash loan + fee // 4. Keep the profit func (e *FlashLoanExecutor) Execute(ctx context.Context, opp *arbitrage.Opportunity) (*types.Transaction, error) { e.logger.Info("executing arbitrage via flash loan", "inputToken", opp.InputToken.Hex(), "bridgeToken", opp.BridgeToken.Hex(), "inputAmount", opp.InputAmount.String(), "profitBPS", opp.ProfitBPS.String(), ) // Check gas price before executing gasPrice, err := e.client.SuggestGasPrice(ctx) if err != nil { return nil, fmt.Errorf("failed to get gas price: %w", err) } if gasPrice.Cmp(e.maxGasPrice) > 0 { return nil, fmt.Errorf("gas price too high: %s > %s", gasPrice.String(), e.maxGasPrice.String()) } // Calculate flash loan fee (Aave V3 charges 0.05% = 5 BPS) flashLoanFee := new(big.Int).Mul(opp.InputAmount, big.NewInt(5)) flashLoanFee.Div(flashLoanFee, big.NewInt(10000)) // Check if profit covers flash loan fee netProfit := new(big.Int).Sub(opp.ProfitAmount, flashLoanFee) if netProfit.Cmp(big.NewInt(0)) <= 0 { return nil, fmt.Errorf("profit does not cover flash loan fee: profit=%s, fee=%s", opp.ProfitAmount.String(), flashLoanFee.String()) } e.logger.Info("flash loan profitability check", "grossProfit", opp.ProfitAmount.String(), "flashLoanFee", flashLoanFee.String(), "netProfit", netProfit.String(), ) // For MVP: Return simulation result (actual execution requires deployed contract) // TODO: Deploy flash loan contract and call executeFlashLoan() e.logger.Warn("flash loan execution not yet implemented - returning simulation", "netProfit", netProfit.String(), ) // Update stats (for simulation) e.executedCount++ e.profitTotal.Add(e.profitTotal, netProfit) return nil, fmt.Errorf("flash loan execution not yet implemented - contract deployment needed") } // SimulateExecution simulates execution without sending transaction // Useful for testing profitability before deploying contracts func (e *FlashLoanExecutor) SimulateExecution(opp *arbitrage.Opportunity) (*SimulationResult, error) { // Calculate flash loan fee (Aave V3: 0.05%) flashLoanFee := new(big.Int).Mul(opp.InputAmount, big.NewInt(5)) flashLoanFee.Div(flashLoanFee, big.NewInt(10000)) // Calculate net profit after flash loan fee netProfit := new(big.Int).Sub(opp.ProfitAmount, flashLoanFee) // Estimate gas cost estimatedGas := new(big.Int).Mul(big.NewInt(int64(e.gasLimit)), e.maxGasPrice) // Calculate final profit after gas finalProfit := new(big.Int).Sub(netProfit, estimatedGas) isProfitable := finalProfit.Cmp(big.NewInt(0)) > 0 return &SimulationResult{ GrossProfit: opp.ProfitAmount, FlashLoanFee: flashLoanFee, NetProfit: netProfit, EstimatedGas: estimatedGas, FinalProfit: finalProfit, IsProfitable: isProfitable, }, nil } // SimulationResult contains the results of a simulated execution type SimulationResult struct { GrossProfit *big.Int // Profit before fees FlashLoanFee *big.Int // Aave flash loan fee (0.05%) NetProfit *big.Int // Profit after flash loan fee EstimatedGas *big.Int // Estimated gas cost in wei FinalProfit *big.Int // Final profit after all costs IsProfitable bool // Whether execution is profitable } // GetStats returns execution statistics func (e *FlashLoanExecutor) GetStats() (executedCount uint64, totalProfit *big.Int) { return e.executedCount, new(big.Int).Set(e.profitTotal) } // NOTE: Flash loan contract implementation needed // Contract should implement: // 1. Aave V3 IFlashLoanSimpleReceiver interface // 2. executeOperation() callback that: // - Swaps token A -> B on pool1 // - Swaps token B -> A on pool2 // - Returns borrowed amount + fee to Aave // - Transfers profit to owner // // Deploy contract code (Solidity): // // pragma solidity ^0.8.0; // // import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; // // contract ArbitrageExecutor is FlashLoanSimpleReceiverBase { // address public owner; // // constructor(address _addressProvider) FlashLoanSimpleReceiverBase(_addressProvider) { // owner = msg.sender; // } // // function executeArbitrage( // address asset, // uint256 amount, // address router1, // address router2, // address[] calldata path1, // address[] calldata path2 // ) external onlyOwner { // bytes memory params = abi.encode(router1, router2, path1, path2); // POOL.flashLoanSimple(address(this), asset, amount, params, 0); // } // // function executeOperation( // address asset, // uint256 amount, // uint256 premium, // address initiator, // bytes calldata params // ) external override returns (bool) { // // Decode parameters // (address router1, address router2, address[] memory path1, address[] memory path2) = // abi.decode(params, (address, address, address[], address[])); // // // Approve routers // IERC20(asset).approve(router1, amount); // // // Execute first swap // uint[] memory amounts1 = IUniswapV2Router02(router1).swapExactTokensForTokens( // amount, // 0, // No minimum for testing (add slippage protection in production!) // path1, // address(this), // block.timestamp // ); // // // Approve second router // IERC20(path1[path1.length - 1]).approve(router2, amounts1[amounts1.length - 1]); // // // Execute second swap // IUniswapV2Router02(router2).swapExactTokensForTokens( // amounts1[amounts1.length - 1], // amount + premium, // Must get back at least borrowed + fee // path2, // address(this), // block.timestamp // ); // // // Approve Aave to take back borrowed amount + fee // uint256 amountOwed = amount + premium; // IERC20(asset).approve(address(POOL), amountOwed); // // // Transfer profit to owner // uint256 profit = IERC20(asset).balanceOf(address(this)) - amountOwed; // if (profit > 0) { // IERC20(asset).transfer(owner, profit); // } // // return true; // } // // modifier onlyOwner() { // require(msg.sender == owner, "Only owner"); // _; // } // }