// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./LiquidityPoolETH.sol"; import "./interfaces/ISwapRouter.sol"; import "./interfaces/ICurvePool.sol"; import "./interfaces/IAggregationRouter.sol"; import "./interfaces/IDodoexRouter.sol"; import "./interfaces/IBalancerVault.sol"; import "./interfaces/IWETH.sol"; import "../../liquidity/interfaces/ILiquidityProvider.sol"; /** * @title EnhancedSwapRouter * @notice Multi-protocol swap router with intelligent routing and decision logic * @dev Supports Uniswap V3, Curve, Dodoex PMM, Balancer, and 1inch aggregation */ contract EnhancedSwapRouter is AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE"); bytes32 public constant ROUTING_MANAGER_ROLE = keccak256("ROUTING_MANAGER_ROLE"); enum SwapProvider { UniswapV3, Curve, Dodoex, Balancer, OneInch } // Protocol addresses address public immutable uniswapV3Router; address public immutable curve3Pool; address public immutable dodoexRouter; address public immutable balancerVault; address public immutable oneInchRouter; // Token addresses address public immutable weth; address public immutable usdt; address public immutable usdc; address public immutable dai; // Routing configuration struct RoutingConfig { SwapProvider[] providers; // Ordered list of providers to try uint256[] sizeThresholds; // Size thresholds in wei bool enabled; } mapping(SwapProvider => bool) public providerEnabled; mapping(uint256 => RoutingConfig) public sizeBasedRouting; // size category => config uint256 public constant SMALL_SWAP_THRESHOLD = 10_000 * 1e18; // $10k uint256 public constant MEDIUM_SWAP_THRESHOLD = 100_000 * 1e18; // $100k // Uniswap V3 fee tiers uint24 public constant FEE_TIER_LOW = 500; uint24 public constant FEE_TIER_MEDIUM = 3000; uint24 public constant FEE_TIER_HIGH = 10000; // Balancer pool IDs (example - would be set via admin) mapping(address => mapping(address => bytes32)) public balancerPoolIds; // tokenIn => tokenOut => poolId // Dodoex PMM pool addresses (tokenIn => tokenOut => PMM pool address) mapping(address => mapping(address => address)) public dodoPoolAddresses; address public dodoLiquidityProvider; /// @dev Uniswap V3 Quoter for on-chain quotes; set via setUniswapQuoter when deployed on 138/651940 address public uniswapQuoter; event SwapExecuted( SwapProvider indexed provider, LiquidityPoolETH.AssetType indexed inputAsset, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 gasUsed ); event RoutingConfigUpdated(uint256 sizeCategory, SwapProvider[] providers); event ProviderToggled(SwapProvider provider, bool enabled); error ZeroAddress(); error ZeroAmount(); error SwapFailed(); error InvalidProvider(); error ProviderDisabled(); error InsufficientOutput(); error InvalidRoutingConfig(); error DodoRouteNotConfigured(); /** * @notice Constructor * @param _uniswapV3Router Uniswap V3 SwapRouter address * @param _curve3Pool Curve 3pool address * @param _dodoexRouter Dodoex Router address * @param _balancerVault Balancer Vault address * @param _oneInchRouter 1inch Router address (can be address(0)) * @param _weth WETH address * @param _usdt USDT address * @param _usdc USDC address * @param _dai DAI address */ constructor( address _uniswapV3Router, address _curve3Pool, address _dodoexRouter, address _balancerVault, address _oneInchRouter, address _weth, address _usdt, address _usdc, address _dai ) { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); if (_uniswapV3Router == address(0) || _curve3Pool == address(0) || _dodoexRouter == address(0) || _balancerVault == address(0) || _weth == address(0) || _usdt == address(0) || _usdc == address(0) || _dai == address(0)) { revert ZeroAddress(); } uniswapV3Router = _uniswapV3Router; curve3Pool = _curve3Pool; dodoexRouter = _dodoexRouter; balancerVault = _balancerVault; oneInchRouter = _oneInchRouter; weth = _weth; usdt = _usdt; usdc = _usdc; dai = _dai; // Enable all providers by default providerEnabled[SwapProvider.UniswapV3] = true; providerEnabled[SwapProvider.Curve] = true; providerEnabled[SwapProvider.Dodoex] = true; providerEnabled[SwapProvider.Balancer] = true; if (_oneInchRouter != address(0)) { providerEnabled[SwapProvider.OneInch] = true; } // Initialize default routing configs _initializeDefaultRouting(); } /** * @notice Swap to stablecoin using intelligent routing * @param inputAsset Input asset type (ETH or WETH) * @param stablecoinToken Target stablecoin * @param amountIn Input amount * @param amountOutMin Minimum output (slippage protection) * @param preferredProvider Optional preferred provider (0 = auto-select) * @return amountOut Output amount * @return providerUsed Provider that executed the swap */ function swapToStablecoin( LiquidityPoolETH.AssetType inputAsset, address stablecoinToken, uint256 amountIn, uint256 amountOutMin, SwapProvider preferredProvider ) external payable nonReentrant returns (uint256 amountOut, SwapProvider providerUsed) { if (amountIn == 0) revert ZeroAmount(); if (stablecoinToken == address(0)) revert ZeroAddress(); if (!_isValidStablecoin(stablecoinToken)) revert("EnhancedSwapRouter: invalid stablecoin"); // Convert ETH to WETH if needed if (inputAsset == LiquidityPoolETH.AssetType.ETH) { require(msg.value == amountIn, "EnhancedSwapRouter: ETH amount mismatch"); IWETH(weth).deposit{value: amountIn}(); } // Get routing providers based on swap size SwapProvider[] memory providers = _getRoutingProviders(amountIn, preferredProvider); // Try each provider in order for (uint256 i = 0; i < providers.length; i++) { if (!providerEnabled[providers[i]]) continue; try this._executeSwap( providers[i], stablecoinToken, amountIn, amountOutMin ) returns (uint256 output) { if (output >= amountOutMin) { // Transfer output to caller IERC20(stablecoinToken).safeTransfer(msg.sender, output); emit SwapExecuted( providers[i], inputAsset, weth, stablecoinToken, amountIn, output, gasleft() ); return (output, providers[i]); } } catch { // Try next provider continue; } } revert SwapFailed(); } /** * @notice Get quote from all enabled providers * @param stablecoinToken Target stablecoin * @param amountIn Input amount * @return providers Array of providers that returned quotes * @return amounts Array of output amounts for each provider */ function getQuotes( address stablecoinToken, uint256 amountIn ) external view returns (SwapProvider[] memory providers, uint256[] memory amounts) { SwapProvider[] memory enabledProviders = new SwapProvider[](5); uint256[] memory quotes = new uint256[](5); uint256 count = 0; // Query each enabled provider if (providerEnabled[SwapProvider.UniswapV3]) { try this._getUniswapV3Quote(stablecoinToken, amountIn) returns (uint256 quote) { enabledProviders[count] = SwapProvider.UniswapV3; quotes[count] = quote; count++; } catch {} } if (providerEnabled[SwapProvider.Dodoex]) { try this._getDodoexQuote(stablecoinToken, amountIn) returns (uint256 quote) { enabledProviders[count] = SwapProvider.Dodoex; quotes[count] = quote; count++; } catch {} } if (providerEnabled[SwapProvider.Balancer]) { try this._getBalancerQuote(stablecoinToken, amountIn) returns (uint256 quote) { enabledProviders[count] = SwapProvider.Balancer; quotes[count] = quote; count++; } catch {} } // Resize arrays SwapProvider[] memory resultProviders = new SwapProvider[](count); uint256[] memory resultQuotes = new uint256[](count); for (uint256 i = 0; i < count; i++) { resultProviders[i] = enabledProviders[i]; resultQuotes[i] = quotes[i]; } return (resultProviders, resultQuotes); } /** * @notice Set routing configuration for a size category * @param sizeCategory 0 = small, 1 = medium, 2 = large * @param providers Ordered list of providers to try */ function setRoutingConfig( uint256 sizeCategory, SwapProvider[] calldata providers ) external onlyRole(ROUTING_MANAGER_ROLE) { require(sizeCategory < 3, "EnhancedSwapRouter: invalid size category"); require(providers.length > 0, "EnhancedSwapRouter: empty providers"); sizeBasedRouting[sizeCategory] = RoutingConfig({ providers: providers, sizeThresholds: new uint256[](0), enabled: true }); emit RoutingConfigUpdated(sizeCategory, providers); } /** * @notice Toggle provider on/off * @param provider Provider to toggle * @param enabled Whether to enable */ function setProviderEnabled( SwapProvider provider, bool enabled ) external onlyRole(ROUTING_MANAGER_ROLE) { providerEnabled[provider] = enabled; emit ProviderToggled(provider, enabled); } /** * @notice Set Balancer pool ID for a token pair * @param tokenIn Input token * @param tokenOut Output token * @param poolId Balancer pool ID */ function setBalancerPoolId( address tokenIn, address tokenOut, bytes32 poolId ) external onlyRole(ROUTING_MANAGER_ROLE) { balancerPoolIds[tokenIn][tokenOut] = poolId; } /** * @notice Set Dodoex PMM pool address for a token pair * @param tokenIn Input token * @param tokenOut Output token * @param poolAddress Dodo PMM pool address */ function setDodoPoolAddress( address tokenIn, address tokenOut, address poolAddress ) external onlyRole(ROUTING_MANAGER_ROLE) { dodoPoolAddresses[tokenIn][tokenOut] = poolAddress; } /** * @notice Set DODO liquidity provider implementation for environments that execute * swaps via a local provider/integration instead of an external Dodoex router. * @param provider Address of ILiquidityProvider-compatible contract */ function setDodoLiquidityProvider(address provider) external onlyRole(ROUTING_MANAGER_ROLE) { dodoLiquidityProvider = provider; } /** * @notice Set Uniswap V3 Quoter address for on-chain quotes * @param _quoter Quoter contract address (address(0) to use 0.5% slippage estimate) */ function setUniswapQuoter(address _quoter) external onlyRole(ROUTING_MANAGER_ROLE) { uniswapQuoter = _quoter; } /** * @notice Swap arbitrary token pair via Dodoex when pool is configured * @param tokenIn Input token * @param tokenOut Output token * @param amountIn Input amount * @param amountOutMin Minimum output (slippage protection) * @return amountOut Output amount */ function swapTokenToToken( address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin ) external nonReentrant returns (uint256 amountOut) { if (amountIn == 0) revert ZeroAmount(); if (tokenIn == address(0) || tokenOut == address(0)) revert ZeroAddress(); address pool = dodoPoolAddresses[tokenIn][tokenOut]; if (!_hasDodoRoute(tokenIn, tokenOut, pool)) revert DodoRouteNotConfigured(); IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); if (dodoLiquidityProvider != address(0)) { IERC20(tokenIn).approve(dodoLiquidityProvider, amountIn); amountOut = ILiquidityProvider(dodoLiquidityProvider).executeSwap( tokenIn, tokenOut, amountIn, amountOutMin ); require(amountOut >= amountOutMin, "EnhancedSwapRouter: insufficient output"); IERC20(tokenOut).safeTransfer(msg.sender, amountOut); return amountOut; } IERC20(tokenIn).approve(dodoexRouter, amountIn); address[] memory dodoPairs = new address[](1); dodoPairs[0] = pool; IDodoexRouter.DodoSwapParams memory params = IDodoexRouter.DodoSwapParams({ fromToken: tokenIn, toToken: tokenOut, fromTokenAmount: amountIn, minReturnAmount: amountOutMin, dodoPairs: dodoPairs, directions: 0, isIncentive: false, deadLine: block.timestamp + 300 }); amountOut = IDodoexRouter(dodoexRouter).dodoSwapV2TokenToToken(params); require(amountOut >= amountOutMin, "EnhancedSwapRouter: insufficient output"); IERC20(tokenOut).safeTransfer(msg.sender, amountOut); return amountOut; } // ============ Internal Functions ============ /** * @notice Execute swap via specified provider */ function _executeSwap( SwapProvider provider, address stablecoinToken, uint256 amountIn, uint256 amountOutMin ) external returns (uint256) { require(msg.sender == address(this), "EnhancedSwapRouter: internal only"); if (provider == SwapProvider.UniswapV3) { return _executeUniswapV3Swap(stablecoinToken, amountIn, amountOutMin); } else if (provider == SwapProvider.Dodoex) { return _executeDodoexSwap(stablecoinToken, amountIn, amountOutMin); } else if (provider == SwapProvider.Balancer) { return _executeBalancerSwap(stablecoinToken, amountIn, amountOutMin); } else if (provider == SwapProvider.Curve) { return _executeCurveSwap(stablecoinToken, amountIn, amountOutMin); } else if (provider == SwapProvider.OneInch && oneInchRouter != address(0)) { return _execute1inchSwap(stablecoinToken, amountIn, amountOutMin); } revert InvalidProvider(); } /** * @notice Get routing providers based on swap size */ function _getRoutingProviders( uint256 amountIn, SwapProvider preferredProvider ) internal view returns (SwapProvider[] memory) { // If preferred provider is specified and enabled, use it first if (preferredProvider != SwapProvider.UniswapV3 && providerEnabled[preferredProvider]) { SwapProvider[] memory providers = new SwapProvider[](1); providers[0] = preferredProvider; return providers; } // Determine size category uint256 category; if (amountIn < SMALL_SWAP_THRESHOLD) { category = 0; // Small } else if (amountIn < MEDIUM_SWAP_THRESHOLD) { category = 1; // Medium } else { category = 2; // Large } RoutingConfig memory config = sizeBasedRouting[category]; if (config.enabled && config.providers.length > 0) { return config.providers; } // Default fallback routing SwapProvider[] memory defaultProviders = new SwapProvider[](5); defaultProviders[0] = SwapProvider.Dodoex; defaultProviders[1] = SwapProvider.UniswapV3; defaultProviders[2] = SwapProvider.Balancer; defaultProviders[3] = SwapProvider.Curve; defaultProviders[4] = SwapProvider.OneInch; return defaultProviders; } /** * @notice Execute Uniswap V3 swap */ function _executeUniswapV3Swap( address stablecoinToken, uint256 amountIn, uint256 amountOutMin ) internal returns (uint256) { // Approve for swap IERC20 wethToken = IERC20(weth); wethToken.approve(uniswapV3Router, amountIn); ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: weth, tokenOut: stablecoinToken, fee: FEE_TIER_MEDIUM, recipient: address(this), deadline: block.timestamp + 300, amountIn: amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0 }); return ISwapRouter(uniswapV3Router).exactInputSingle(params); } /** * @notice Execute Dodoex PMM swap */ function _executeDodoexSwap( address stablecoinToken, uint256 amountIn, uint256 amountOutMin ) internal returns (uint256) { address pool = dodoPoolAddresses[weth][stablecoinToken]; if (!_hasDodoRoute(weth, stablecoinToken, pool)) revert DodoRouteNotConfigured(); if (dodoLiquidityProvider != address(0)) { IERC20 wethTokenViaProvider = IERC20(weth); wethTokenViaProvider.approve(dodoLiquidityProvider, amountIn); return ILiquidityProvider(dodoLiquidityProvider).executeSwap( weth, stablecoinToken, amountIn, amountOutMin ); } IERC20 wethToken = IERC20(weth); wethToken.approve(dodoexRouter, amountIn); address[] memory dodoPairs = new address[](1); dodoPairs[0] = pool; IDodoexRouter.DodoSwapParams memory params = IDodoexRouter.DodoSwapParams({ fromToken: weth, toToken: stablecoinToken, fromTokenAmount: amountIn, minReturnAmount: amountOutMin, dodoPairs: dodoPairs, directions: 0, isIncentive: false, deadLine: block.timestamp + 300 }); return IDodoexRouter(dodoexRouter).dodoSwapV2TokenToToken(params); } /** * @notice Execute Balancer swap */ function _executeBalancerSwap( address stablecoinToken, uint256 amountIn, uint256 amountOutMin ) internal returns (uint256) { bytes32 poolId = balancerPoolIds[weth][stablecoinToken]; require(poolId != bytes32(0), "EnhancedSwapRouter: pool not configured"); IERC20 wethToken = IERC20(weth); wethToken.approve(balancerVault, amountIn); IBalancerVault.SingleSwap memory singleSwap = IBalancerVault.SingleSwap({ poolId: poolId, kind: IBalancerVault.SwapKind.GIVEN_IN, assetIn: weth, assetOut: stablecoinToken, amount: amountIn, userData: "" }); IBalancerVault.FundManagement memory funds = IBalancerVault.FundManagement({ sender: address(this), fromInternalBalance: false, recipient: payable(address(this)), toInternalBalance: false }); return IBalancerVault(balancerVault).swap( singleSwap, funds, amountOutMin, block.timestamp + 300 ); } /** * @notice Execute Curve swap */ function _executeCurveSwap( address, uint256, uint256 ) internal pure returns (uint256) { // Curve 3pool doesn't support WETH directly // Would need intermediate swap or different pool revert("EnhancedSwapRouter: Curve direct swap not supported"); } /** * @notice Execute 1inch swap */ function _execute1inchSwap( address, uint256 amountIn, uint256 ) internal returns (uint256) { if (oneInchRouter == address(0)) revert ProviderDisabled(); IERC20 wethToken = IERC20(weth); wethToken.approve(oneInchRouter, amountIn); // 1inch: requires route data from 1inch API (e.g. /swap/v5.2/138/swap). Use 1inch SDK or API to get calldata and execute separately. revert("EnhancedSwapRouter: 1inch requires route calldata from API; use 1inch aggregator SDK"); } /** * @notice Get Uniswap V3 quote (view) * When UNISWAP_QUOTER_ADDRESS is configured, queries quoter. Otherwise returns * estimate via amountIn * 9950/10000 (0.5% slippage) for stablecoin pairs. */ function _getUniswapV3Quote( address stablecoinToken, uint256 amountIn ) external view returns (uint256) { if (uniswapQuoter != address(0) && _isValidStablecoin(stablecoinToken)) { // IQuoter.quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, sqrtPriceLimitX96) (bool ok, bytes memory data) = uniswapQuoter.staticcall( abi.encodeWithSignature( "quoteExactInputSingle(address,address,uint24,uint256,uint160)", weth, stablecoinToken, FEE_TIER_MEDIUM, amountIn, uint160(0) ) ); if (ok && data.length >= 32) { uint256 quoted = abi.decode(data, (uint256)); if (quoted > 0) return quoted; } } if (_isValidStablecoin(stablecoinToken)) { return (amountIn * 9950) / 10000; // 0.5% slippage estimate for WETH->stable } return 0; } /** * @notice Get Dodoex quote (view) */ function _getDodoexQuote( address stablecoinToken, uint256 amountIn ) external view returns (uint256) { if (dodoLiquidityProvider != address(0)) { if (!ILiquidityProvider(dodoLiquidityProvider).supportsTokenPair(weth, stablecoinToken)) { return 0; } (uint256 amountOut,) = ILiquidityProvider(dodoLiquidityProvider).getQuote(weth, stablecoinToken, amountIn); return amountOut; } if (dodoPoolAddresses[weth][stablecoinToken] == address(0)) { return 0; } return IDodoexRouter(dodoexRouter).getDodoSwapQuote(weth, stablecoinToken, amountIn); } function _hasDodoRoute( address tokenIn, address tokenOut, address configuredPool ) internal view returns (bool) { if (dodoLiquidityProvider != address(0)) { return ILiquidityProvider(dodoLiquidityProvider).supportsTokenPair(tokenIn, tokenOut); } return configuredPool != address(0); } /** * @notice Get Balancer quote (view) * When poolId configured, would query Balancer. Otherwise estimate for stablecoins. */ function _getBalancerQuote( address stablecoinToken, uint256 amountIn ) external view returns (uint256) { bytes32 poolId = balancerPoolIds[weth][stablecoinToken]; if (poolId != bytes32(0)) { (address[] memory tokens, uint256[] memory balances,) = IBalancerVault(balancerVault).getPoolTokens(poolId); if (tokens.length >= 2 && balances.length >= 2) { uint256 wethIdx = type(uint256).max; uint256 stableIdx = type(uint256).max; for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == weth) wethIdx = i; if (tokens[i] == stablecoinToken) stableIdx = i; } if (wethIdx != type(uint256).max && stableIdx != type(uint256).max && balances[wethIdx] > 0) { uint256 amountOut = (amountIn * balances[stableIdx]) / balances[wethIdx]; return (amountOut * 9950) / 10000; // 0.5% slippage } } } if (_isValidStablecoin(stablecoinToken)) { return (amountIn * 9950) / 10000; // 0.5% slippage estimate when pool not configured } return 0; } /** * @notice Initialize default routing configurations */ function _initializeDefaultRouting() internal { // Small swaps (< $10k): Uniswap V3, Dodoex SwapProvider[] memory smallProviders = new SwapProvider[](2); smallProviders[0] = SwapProvider.UniswapV3; smallProviders[1] = SwapProvider.Dodoex; sizeBasedRouting[0] = RoutingConfig({ providers: smallProviders, sizeThresholds: new uint256[](0), enabled: true }); // Medium swaps ($10k-$100k): Dodoex, Balancer, Uniswap V3 SwapProvider[] memory mediumProviders = new SwapProvider[](3); mediumProviders[0] = SwapProvider.Dodoex; mediumProviders[1] = SwapProvider.Balancer; mediumProviders[2] = SwapProvider.UniswapV3; sizeBasedRouting[1] = RoutingConfig({ providers: mediumProviders, sizeThresholds: new uint256[](0), enabled: true }); // Large swaps (> $100k): Dodoex, Curve, Balancer SwapProvider[] memory largeProviders = new SwapProvider[](3); largeProviders[0] = SwapProvider.Dodoex; largeProviders[1] = SwapProvider.Curve; largeProviders[2] = SwapProvider.Balancer; sizeBasedRouting[2] = RoutingConfig({ providers: largeProviders, sizeThresholds: new uint256[](0), enabled: true }); } /** * @notice Check if token is valid stablecoin */ function _isValidStablecoin(address token) internal view returns (bool) { return token == usdt || token == usdc || token == dai; } // Allow contract to receive ETH receive() external payable {} }