Files
mev-beta/contracts/ProductionArbitrageExecutor.sol
2025-10-04 09:31:02 -05:00

613 lines
23 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
interface IUniswapV3Pool {
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
function token0() external view returns (address);
function token1() external view returns (address);
function fee() external view returns (uint24);
}
interface IUniswapV3Router {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
}
interface ICamelotRouter {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
address referrer,
uint deadline
) external returns (uint[] memory amounts);
function getAmountsOut(uint amountIn, address[] calldata path)
external view returns (uint[] memory amounts);
}
/**
* @title ProductionArbitrageExecutor
* @dev PRODUCTION-GRADE arbitrage executor for profitable MEV extraction
* @notice This contract executes flash swap arbitrage between DEXes on Arbitrum
*/
contract ProductionArbitrageExecutor is ReentrancyGuard, AccessControl, Pausable {
using SafeERC20 for IERC20;
using Address for address;
// Role definitions for access control
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
// Factory address for pool validation
address public constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
// Maximum slippage tolerance (5% = 500 basis points)
uint256 public constant MAX_SLIPPAGE_BPS = 500;
// Mapping to track authorized pools for flash loans
mapping(address => bool) public authorizedPools;
// Circuit breaker for emergency stops
bool public emergencyStop = false;
// Router addresses on Arbitrum
IUniswapV3Router public constant UNISWAP_V3_ROUTER =
IUniswapV3Router(0xE592427A0AEce92De3Edee1F18E0157C05861564);
ICamelotRouter public constant CAMELOT_ROUTER =
ICamelotRouter(0xc873fEcbd354f5A56E00E710B90EF4201db2448d);
// Common token addresses on Arbitrum
address public constant WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
address public constant USDC = 0xaF88d065e77c8cC2239327C5EDb3A432268e5831;
address public constant USDT = 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9;
address public constant ARB = 0x912CE59144191C1204E64559FE8253a0e49E6548;
// Minimum profit threshold (in wei)
uint256 public minProfitThreshold = 0.005 ether; // 0.005 ETH minimum profit
// Maximum gas price for profitable execution
uint256 public maxGasPrice = 5 gwei; // 5 gwei max
// Events for tracking profitable arbitrage
event ArbitrageExecuted(
address indexed tokenA,
address indexed tokenB,
uint256 amountIn,
uint256 profit,
uint256 gasUsed,
address indexed executor
);
event ProfitWithdrawn(address indexed token, uint256 amount, address indexed recipient);
event PoolAuthorized(address indexed pool, bool authorized);
event EmergencyStopToggled(bool stopped);
event SlippageExceeded(address indexed pool, uint256 expectedAmount, uint256 actualAmount);
struct ArbitrageParams {
address tokenA;
address tokenB;
uint256 amountIn;
uint24 uniswapFee;
address[] camelotPath;
uint256 minProfit;
uint256 maxSlippageBps; // Maximum allowed slippage in basis points
bool buyOnUniswap; // true = buy on Uniswap, sell on Camelot
uint256 deadline; // Transaction deadline for additional safety
}
/**
* @dev Constructor sets up roles and initial configuration
*/
constructor(address admin, address executor) {
require(admin != address(0) && executor != address(0), "Invalid addresses");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ADMIN_ROLE, admin);
_grantRole(EXECUTOR_ROLE, executor);
_grantRole(EMERGENCY_ROLE, admin);
// Set role admin relationships
_setRoleAdmin(EXECUTOR_ROLE, ADMIN_ROLE);
_setRoleAdmin(EMERGENCY_ROLE, ADMIN_ROLE);
}
/**
* @dev Authorize a pool for flash loans
*/
function authorizePool(address pool) external onlyRole(ADMIN_ROLE) {
require(pool != address(0), "Invalid pool address");
require(pool.isContract(), "Pool must be a contract");
authorizedPools[pool] = true;
emit PoolAuthorized(pool, true);
}
/**
* @dev Deauthorize a pool for flash loans
*/
function deauthorizePool(address pool) external onlyRole(ADMIN_ROLE) {
authorizedPools[pool] = false;
emit PoolAuthorized(pool, false);
}
/**
* @dev Emergency stop toggle
*/
function toggleEmergencyStop() external onlyRole(EMERGENCY_ROLE) {
emergencyStop = !emergencyStop;
emit EmergencyStopToggled(emergencyStop);
}
/**
* @dev Execute profitable arbitrage using flash swap with comprehensive security checks
* @param pool Uniswap V3 pool to flash swap from
* @param params Arbitrage parameters encoded as bytes
*/
function executeArbitrage(address pool, bytes calldata params)
external
onlyRole(EXECUTOR_ROLE)
nonReentrant
whenNotPaused
{
require(!emergencyStop, "Emergency stop activated");
require(authorizedPools[pool], "Pool not authorized");
require(tx.gasprice <= maxGasPrice, "Gas price too high for profit");
require(pool.isContract(), "Invalid pool address");
ArbitrageParams memory arbParams = abi.decode(params, (ArbitrageParams));
// Comprehensive parameter validation
_validateArbitrageParams(arbParams);
// Validate minimum profit potential
uint256 estimatedProfit = estimateProfit(arbParams);
require(estimatedProfit >= minProfitThreshold, "Insufficient profit potential");
require(estimatedProfit >= arbParams.minProfit, "Below user-specified minimum profit");
// Ensure deadline hasn't passed
require(block.timestamp <= arbParams.deadline, "Transaction deadline exceeded");
// Calculate optimal flash amount with safety checks
uint256 flashAmount = calculateOptimalAmount(arbParams);
require(flashAmount > 0 && flashAmount <= arbParams.amountIn * 2, "Invalid flash amount");
// Prepare flash swap data with additional validation data
bytes memory flashData = abi.encode(arbParams, block.timestamp, msg.sender);
// Execute flash swap with proper token validation
require(_isValidPoolTokens(pool, arbParams.tokenA, arbParams.tokenB), "Invalid pool tokens");
if (arbParams.tokenA == IUniswapV3Pool(pool).token0()) {
IUniswapV3Pool(pool).flash(address(this), flashAmount, 0, flashData);
} else {
IUniswapV3Pool(pool).flash(address(this), 0, flashAmount, flashData);
}
}
/**
* @dev Validate arbitrage parameters comprehensively
*/
function _validateArbitrageParams(ArbitrageParams memory params) private pure {
require(params.tokenA != address(0) && params.tokenB != address(0), "Invalid token addresses");
require(params.tokenA != params.tokenB, "Tokens must be different");
require(params.amountIn > 0, "Amount must be positive");
require(params.minProfit > 0, "Minimum profit must be positive");
require(params.maxSlippageBps <= MAX_SLIPPAGE_BPS, "Slippage tolerance too high");
require(params.camelotPath.length >= 2, "Invalid Camelot path");
require(params.deadline > 0, "Invalid deadline");
require(params.uniswapFee == 500 || params.uniswapFee == 3000 || params.uniswapFee == 10000, "Invalid Uniswap fee");
}
/**
* @dev Validate pool tokens match arbitrage parameters
*/
function _isValidPoolTokens(address pool, address tokenA, address tokenB) private view returns (bool) {
try IUniswapV3Pool(pool).token0() returns (address token0) {
try IUniswapV3Pool(pool).token1() returns (address token1) {
return (token0 == tokenA && token1 == tokenB) || (token0 == tokenB && token1 == tokenA);
} catch {
return false;
}
} catch {
return false;
}
}
/**
* @dev Uniswap V3 flash callback with enhanced security validation
*/
function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external nonReentrant {
uint256 gasStart = gasleft();
// Critical: Validate callback is from authorized pool FIRST
require(authorizedPools[msg.sender], "Unauthorized pool callback");
require(!emergencyStop, "Emergency stop activated");
(ArbitrageParams memory params, uint256 timestamp, address originalCaller) =
abi.decode(data, (ArbitrageParams, uint256, address));
// Enhanced callback validation
require(_isValidPoolTokens(msg.sender, params.tokenA, params.tokenB), "Invalid pool tokens");
require(block.timestamp <= timestamp + 300, "Callback too old");
require(block.timestamp <= params.deadline, "Transaction deadline exceeded");
// Calculate amount owed with overflow protection
uint256 flashFee = params.tokenA == IUniswapV3Pool(msg.sender).token0() ? fee0 : fee1;
uint256 amountOwed;
require(params.amountIn + flashFee >= params.amountIn, "Overflow in amount calculation");
amountOwed = params.amountIn + flashFee;
// Ensure we have sufficient balance for flash loan execution
uint256 initialBalance = IERC20(params.tokenA).balanceOf(address(this));
require(initialBalance >= params.amountIn, "Insufficient flash loan balance");
// Execute arbitrage strategy with slippage protection
uint256 profit = executeArbitrageStrategy(params, amountOwed);
// Enhanced profit validation
require(profit >= minProfitThreshold, "Below minimum profit threshold");
require(profit >= params.minProfit, "Below user-specified minimum");
// Ensure we can repay the flash loan
uint256 finalBalance = IERC20(params.tokenA).balanceOf(address(this));
require(finalBalance >= amountOwed, "Insufficient balance to repay flash loan");
// Repay flash loan with precise amount
IERC20(params.tokenA).safeTransfer(msg.sender, amountOwed);
// Calculate actual gas cost
uint256 gasUsed = gasStart - gasleft();
emit ArbitrageExecuted(
params.tokenA,
params.tokenB,
params.amountIn,
profit,
gasUsed,
originalCaller
);
}
/**
* @dev Execute the actual arbitrage strategy
*/
function executeArbitrageStrategy(ArbitrageParams memory params, uint256 amountOwed)
private returns (uint256 profit) {
uint256 startBalance = IERC20(params.tokenA).balanceOf(address(this));
if (params.buyOnUniswap) {
// Buy tokenB on Uniswap, sell on Camelot
uint256 amountOut = buyOnUniswap(params);
uint256 finalAmount = sellOnCamelot(params.tokenB, params.tokenA, amountOut, params.camelotPath);
uint256 endBalance = IERC20(params.tokenA).balanceOf(address(this));
profit = endBalance > startBalance + amountOwed ?
endBalance - startBalance - amountOwed : 0;
} else {
// Buy tokenB on Camelot, sell on Uniswap
uint256 amountOut = buyOnCamelot(params);
uint256 finalAmount = sellOnUniswap(params.tokenB, params.tokenA, amountOut, params.uniswapFee);
uint256 endBalance = IERC20(params.tokenA).balanceOf(address(this));
profit = endBalance > startBalance + amountOwed ?
endBalance - startBalance - amountOwed : 0;
}
}
/**
* @dev Buy tokens on Uniswap V3 with precise approvals and slippage protection
*/
function buyOnUniswap(ArbitrageParams memory params) private returns (uint256 amountOut) {
// Reset any existing approval to 0 first (for certain tokens like USDT)
IERC20(params.tokenA).safeApprove(address(UNISWAP_V3_ROUTER), 0);
// Approve exact amount needed, no more
IERC20(params.tokenA).safeApprove(address(UNISWAP_V3_ROUTER), params.amountIn);
// Calculate minimum amount out with slippage protection
uint256 minAmountOut = _calculateMinAmountOut(params.amountIn, params.maxSlippageBps);
IUniswapV3Router.ExactInputSingleParams memory swapParams = IUniswapV3Router.ExactInputSingleParams({
tokenIn: params.tokenA,
tokenOut: params.tokenB,
fee: params.uniswapFee,
recipient: address(this),
deadline: params.deadline,
amountIn: params.amountIn,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: 0
});
amountOut = UNISWAP_V3_ROUTER.exactInputSingle(swapParams);
// Reset approval after use for security
IERC20(params.tokenA).safeApprove(address(UNISWAP_V3_ROUTER), 0);
// Validate slippage didn't exceed expectations
require(amountOut >= minAmountOut, "Excessive slippage detected");
}
/**
* @dev Sell tokens on Uniswap V3 with precise approvals and slippage protection
*/
function sellOnUniswap(address tokenIn, address tokenOut, uint256 amountIn, uint24 fee)
private returns (uint256 amountOut) {
// Reset any existing approval to 0 first
IERC20(tokenIn).safeApprove(address(UNISWAP_V3_ROUTER), 0);
// Approve exact amount needed
IERC20(tokenIn).safeApprove(address(UNISWAP_V3_ROUTER), amountIn);
// Calculate minimum amount out with slippage protection (assuming 0.5% slippage for simplicity)
uint256 minAmountOut = (amountIn * 9950) / 10000; // 0.5% slippage
IUniswapV3Router.ExactInputSingleParams memory swapParams = IUniswapV3Router.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: address(this),
deadline: block.timestamp + 300,
amountIn: amountIn,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: 0
});
amountOut = UNISWAP_V3_ROUTER.exactInputSingle(swapParams);
// Reset approval after use
IERC20(tokenIn).safeApprove(address(UNISWAP_V3_ROUTER), 0);
require(amountOut >= minAmountOut, "Excessive slippage on Uniswap sell");
}
/**
* @dev Buy tokens on Camelot with precise approvals and slippage protection
*/
function buyOnCamelot(ArbitrageParams memory params) private returns (uint256 amountOut) {
// Reset approval to 0 first
IERC20(params.tokenA).safeApprove(address(CAMELOT_ROUTER), 0);
// Approve exact amount needed
IERC20(params.tokenA).safeApprove(address(CAMELOT_ROUTER), params.amountIn);
// Calculate minimum amount out with slippage protection
uint256 minAmountOut = _calculateMinAmountOut(params.amountIn, params.maxSlippageBps);
uint256[] memory amounts = CAMELOT_ROUTER.swapExactTokensForTokens(
params.amountIn,
minAmountOut,
params.camelotPath,
address(this),
address(0), // referrer
params.deadline
);
amountOut = amounts[amounts.length - 1];
// Reset approval after use
IERC20(params.tokenA).safeApprove(address(CAMELOT_ROUTER), 0);
require(amountOut >= minAmountOut, "Excessive slippage on Camelot buy");
}
/**
* @dev Sell tokens on Camelot with precise approvals and slippage protection
*/
function sellOnCamelot(address tokenIn, address tokenOut, uint256 amountIn, address[] memory path)
private returns (uint256 amountOut) {
// Reset approval to 0 first
IERC20(tokenIn).safeApprove(address(CAMELOT_ROUTER), 0);
// Approve exact amount needed
IERC20(tokenIn).safeApprove(address(CAMELOT_ROUTER), amountIn);
// Calculate minimum amount out with slippage protection (0.5% slippage)
uint256 minAmountOut = (amountIn * 9950) / 10000;
uint256[] memory amounts = CAMELOT_ROUTER.swapExactTokensForTokens(
amountIn,
minAmountOut,
path,
address(this),
address(0),
block.timestamp + 300
);
amountOut = amounts[amounts.length - 1];
// Reset approval after use
IERC20(tokenIn).safeApprove(address(CAMELOT_ROUTER), 0);
require(amountOut >= minAmountOut, "Excessive slippage on Camelot sell");
}
/**
* @dev Estimate profit for given arbitrage parameters
*/
function estimateProfit(ArbitrageParams memory params) public view returns (uint256 profit) {
// This is a simplified estimation - in production you'd use more sophisticated pricing
try CAMELOT_ROUTER.getAmountsOut(params.amountIn, params.camelotPath) returns (uint256[] memory amounts) {
uint256 camelotOutput = amounts[amounts.length - 1];
// Estimate Uniswap output (simplified)
uint256 uniswapOutput = params.amountIn * 995 / 1000; // Rough estimate with 0.5% slippage
if (params.buyOnUniswap && camelotOutput > params.amountIn) {
profit = camelotOutput - params.amountIn;
} else if (!params.buyOnUniswap && uniswapOutput > params.amountIn) {
profit = uniswapOutput - params.amountIn;
}
// Subtract estimated gas costs (0.002 ETH equivalent)
uint256 gasCostInToken = 0.002 ether; // Rough estimate
profit = profit > gasCostInToken ? profit - gasCostInToken : 0;
} catch {
profit = 0;
}
}
/**
* @dev Calculate optimal flash swap amount for maximum profit
*/
function calculateOptimalAmount(ArbitrageParams memory params) public view returns (uint256) {
// Start with base amount and find optimal size
uint256 baseAmount = 1 ether; // 1 token base
uint256 maxProfit = 0;
uint256 optimalAmount = baseAmount;
// Test different amounts to find optimal
for (uint256 multiplier = 1; multiplier <= 10; multiplier++) {
uint256 testAmount = baseAmount * multiplier;
ArbitrageParams memory testParams = params;
testParams.amountIn = testAmount;
uint256 estimatedProfit = estimateProfit(testParams);
if (estimatedProfit > maxProfit && estimatedProfit >= minProfitThreshold) {
maxProfit = estimatedProfit;
optimalAmount = testAmount;
}
}
return optimalAmount;
}
/**
* @dev Validate that the callback is from a legitimate Uniswap V3 pool
*/
function isValidPool(address pool, address tokenA, address tokenB) private view returns (bool) {
try IUniswapV3Pool(pool).token0() returns (address token0) {
try IUniswapV3Pool(pool).token1() returns (address token1) {
return (token0 == tokenA && token1 == tokenB) || (token0 == tokenB && token1 == tokenA);
} catch {
return false;
}
} catch {
return false;
}
}
/**
* @dev Calculate minimum amount out based on slippage tolerance
*/
function _calculateMinAmountOut(uint256 amountIn, uint256 slippageBps) private pure returns (uint256) {
require(slippageBps <= MAX_SLIPPAGE_BPS, "Slippage too high");
return (amountIn * (10000 - slippageBps)) / 10000;
}
/**
* @dev Withdraw accumulated profits with enhanced security
*/
function withdrawProfits(address token, uint256 amount)
external
onlyRole(ADMIN_ROLE)
nonReentrant
{
require(token != address(0), "Invalid token address");
require(amount > 0, "Amount must be positive");
uint256 balance = IERC20(token).balanceOf(address(this));
require(balance >= amount, "Insufficient balance");
require(amount <= balance, "Amount exceeds balance");
address admin = getRoleMember(ADMIN_ROLE, 0);
IERC20(token).safeTransfer(admin, amount);
emit ProfitWithdrawn(token, amount, admin);
}
/**
* @dev Emergency withdrawal function with strict access control
*/
function emergencyWithdraw(address token, uint256 amount)
external
onlyRole(EMERGENCY_ROLE)
nonReentrant
{
require(!emergencyStop, "Emergency stop active");
require(token != address(0), "Invalid token");
require(amount > 0, "Invalid amount");
uint256 balance = IERC20(token).balanceOf(address(this));
require(balance >= amount, "Insufficient balance");
address emergencyAdmin = getRoleMember(EMERGENCY_ROLE, 0);
IERC20(token).safeTransfer(emergencyAdmin, amount);
emit ProfitWithdrawn(token, amount, emergencyAdmin);
}
/**
* @dev Update minimum profit threshold with validation
*/
function setMinProfitThreshold(uint256 _minProfitThreshold) external onlyRole(ADMIN_ROLE) {
require(_minProfitThreshold > 0, "Threshold must be positive");
require(_minProfitThreshold <= 1 ether, "Threshold too high");
minProfitThreshold = _minProfitThreshold;
}
/**
* @dev Update maximum gas price with validation
*/
function setMaxGasPrice(uint256 _maxGasPrice) external onlyRole(ADMIN_ROLE) {
require(_maxGasPrice > 0, "Gas price must be positive");
require(_maxGasPrice <= 50 gwei, "Gas price too high");
maxGasPrice = _maxGasPrice;
}
/**
* @dev Pause contract in case of emergency
*/
function pause() external onlyRole(EMERGENCY_ROLE) {
_pause();
}
/**
* @dev Unpause contract
*/
function unpause() external onlyRole(ADMIN_ROLE) {
_unpause();
}
/**
* @dev Receive ETH
*/
receive() external payable {}
}