// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./LiquidityPoolETH.sol"; import "./RouteTypesV2.sol"; import "./interfaces/IRouteExecutorAdapter.sol"; import "./interfaces/IWETH.sol"; contract EnhancedSwapRouterV2 is AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant ROUTING_MANAGER_ROLE = keccak256("ROUTING_MANAGER_ROLE"); bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE"); struct ProviderRouteConfig { address target; bytes providerData; bool enabled; } address public immutable weth; address public immutable usdt; address public immutable usdc; address public immutable dai; mapping(uint8 => address) public providerAdapters; mapping(uint8 => bool) public providerEnabled; mapping(address => mapping(address => mapping(uint8 => ProviderRouteConfig))) private providerRoutes; mapping(uint256 => RouteTypesV2.Provider[]) private sizeBasedRouting; event ProviderAdapterSet(RouteTypesV2.Provider indexed provider, address indexed adapter); event ProviderRouteSet( address indexed tokenIn, address indexed tokenOut, RouteTypesV2.Provider indexed provider, address target, bool enabled ); event ProviderToggled(RouteTypesV2.Provider indexed provider, bool enabled); event RoutingConfigUpdated(uint256 indexed sizeCategory, RouteTypesV2.Provider[] providers); event RouteExecuted( bytes32 indexed routeHash, address indexed caller, address indexed recipient, address inputToken, address outputToken, uint256 amountIn, uint256 amountOut ); error ZeroAddress(); error ZeroAmount(); error InvalidPlan(); error InvalidStablecoin(); error ProviderDisabled(); error ProviderAdapterNotConfigured(); error RouteNotConfigured(); error RouteValidationFailed(string reason); error InsufficientOutput(); constructor(address _weth, address _usdt, address _usdc, address _dai) { if (_weth == address(0) || _usdt == address(0) || _usdc == address(0) || _dai == address(0)) { revert ZeroAddress(); } _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ROUTING_MANAGER_ROLE, msg.sender); weth = _weth; usdt = _usdt; usdc = _usdc; dai = _dai; providerEnabled[uint8(RouteTypesV2.Provider.Dodo)] = true; providerEnabled[uint8(RouteTypesV2.Provider.UniswapV3)] = true; providerEnabled[uint8(RouteTypesV2.Provider.Balancer)] = true; providerEnabled[uint8(RouteTypesV2.Provider.Curve)] = true; providerEnabled[uint8(RouteTypesV2.Provider.OneInch)] = false; providerEnabled[uint8(RouteTypesV2.Provider.Partner)] = false; providerEnabled[uint8(RouteTypesV2.Provider.DodoV3)] = true; _initializeDefaultRouting(); } function setProviderAdapter( RouteTypesV2.Provider provider, address adapter ) external onlyRole(ROUTING_MANAGER_ROLE) { if (adapter == address(0)) revert ZeroAddress(); providerAdapters[uint8(provider)] = adapter; emit ProviderAdapterSet(provider, adapter); } function setProviderRoute( address tokenIn, address tokenOut, RouteTypesV2.Provider provider, address target, bytes calldata providerData, bool enabled ) external onlyRole(ROUTING_MANAGER_ROLE) { if (tokenIn == address(0) || tokenOut == address(0) || target == address(0)) revert ZeroAddress(); providerRoutes[tokenIn][tokenOut][uint8(provider)] = ProviderRouteConfig({ target: target, providerData: providerData, enabled: enabled }); emit ProviderRouteSet(tokenIn, tokenOut, provider, target, enabled); } function getProviderRoute( address tokenIn, address tokenOut, RouteTypesV2.Provider provider ) external view returns (address target, bytes memory providerData, bool enabled) { ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][uint8(provider)]; return (config.target, config.providerData, config.enabled); } function setProviderEnabled( RouteTypesV2.Provider provider, bool enabled ) external onlyRole(ROUTING_MANAGER_ROLE) { providerEnabled[uint8(provider)] = enabled; emit ProviderToggled(provider, enabled); } function setRoutingConfig( uint256 sizeCategory, RouteTypesV2.Provider[] calldata providers ) external onlyRole(ROUTING_MANAGER_ROLE) { require(sizeCategory < 3, "EnhancedSwapRouterV2: invalid size category"); require(providers.length > 0, "EnhancedSwapRouterV2: empty providers"); delete sizeBasedRouting[sizeCategory]; for (uint256 i = 0; i < providers.length; i++) { sizeBasedRouting[sizeCategory].push(providers[i]); } emit RoutingConfigUpdated(sizeCategory, providers); } function executeRoute( RouteTypesV2.RoutePlan calldata plan ) external payable nonReentrant returns (uint256 amountOut) { RouteTypesV2.RoutePlan memory memoryPlan = _copyPlan(plan); return _executeRoute(memoryPlan, msg.sender, msg.value); } function quoteConfiguredProviders( address tokenIn, address tokenOut, uint256 amountIn ) external view returns (RouteTypesV2.ProviderQuote[] memory quotes) { RouteTypesV2.ProviderQuote[] memory temp = new RouteTypesV2.ProviderQuote[](uint256(type(RouteTypesV2.Provider).max) + 1); uint256 count = 0; for (uint8 providerId = 0; providerId <= uint8(type(RouteTypesV2.Provider).max); providerId++) { if (!providerEnabled[providerId]) { continue; } address adapter = providerAdapters[providerId]; if (adapter == address(0)) { continue; } ProviderRouteConfig storage routeConfig = providerRoutes[tokenIn][tokenOut][providerId]; if (!routeConfig.enabled || routeConfig.target == address(0)) { continue; } RouteTypesV2.RouteLeg memory leg = RouteTypesV2.RouteLeg({ provider: RouteTypesV2.Provider(providerId), tokenIn: tokenIn, tokenOut: tokenOut, amountSource: RouteTypesV2.AmountSource.UserInput, minAmountOut: 0, target: routeConfig.target, providerData: routeConfig.providerData }); (bool ok,) = IRouteExecutorAdapter(adapter).validate(leg); if (!ok) { continue; } (uint256 amountOut, uint256 estimatedGas) = IRouteExecutorAdapter(adapter).quote(leg, amountIn); temp[count] = RouteTypesV2.ProviderQuote({ provider: RouteTypesV2.Provider(providerId), target: routeConfig.target, amountOut: amountOut, estimatedGas: estimatedGas, executable: amountOut > 0, providerData: routeConfig.providerData }); count++; } quotes = new RouteTypesV2.ProviderQuote[](count); for (uint256 i = 0; i < count; i++) { quotes[i] = temp[i]; } } function swapTokenToToken( address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin ) external returns (uint256 amountOut) { return swapTokenToToken(tokenIn, tokenOut, amountIn, amountOutMin, _defaultPreferredProvider()); } function swapTokenToToken( address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin, RouteTypesV2.Provider preferredProvider ) public returns (uint256 amountOut) { if (amountIn == 0) revert ZeroAmount(); ProviderRouteConfig memory routeConfig = _getRouteConfig(tokenIn, tokenOut, preferredProvider); RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1); legs[0] = RouteTypesV2.RouteLeg({ provider: preferredProvider, tokenIn: tokenIn, tokenOut: tokenOut, amountSource: RouteTypesV2.AmountSource.UserInput, minAmountOut: amountOutMin, target: routeConfig.target, providerData: routeConfig.providerData }); RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({ chainId: block.chainid, inputToken: tokenIn, outputToken: tokenOut, amountIn: amountIn, minAmountOut: amountOutMin, recipient: msg.sender, deadline: block.timestamp + 300, legs: legs }); return _executeRoute(plan, msg.sender, 0); } function swapToStablecoin( LiquidityPoolETH.AssetType inputAsset, address stablecoinToken, uint256 amountIn, uint256 amountOutMin ) external payable returns (uint256 amountOut) { return swapToStablecoin(inputAsset, stablecoinToken, amountIn, amountOutMin, _defaultPreferredProvider()); } function swapToStablecoin( LiquidityPoolETH.AssetType inputAsset, address stablecoinToken, uint256 amountIn, uint256 amountOutMin, RouteTypesV2.Provider preferredProvider ) public payable returns (uint256 amountOut) { if (amountIn == 0) revert ZeroAmount(); if (!_isValidStablecoin(stablecoinToken)) revert InvalidStablecoin(); address inputToken = inputAsset == LiquidityPoolETH.AssetType.ETH ? address(0) : weth; ProviderRouteConfig memory routeConfig = _getRouteConfig(weth, stablecoinToken, preferredProvider); RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1); legs[0] = RouteTypesV2.RouteLeg({ provider: preferredProvider, tokenIn: weth, tokenOut: stablecoinToken, amountSource: RouteTypesV2.AmountSource.UserInput, minAmountOut: amountOutMin, target: routeConfig.target, providerData: routeConfig.providerData }); RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({ chainId: block.chainid, inputToken: inputToken, outputToken: stablecoinToken, amountIn: amountIn, minAmountOut: amountOutMin, recipient: msg.sender, deadline: block.timestamp + 300, legs: legs }); return _executeRoute(plan, msg.sender, msg.value); } function _executeRoute( RouteTypesV2.RoutePlan memory plan, address payer, uint256 nativeValue ) internal returns (uint256 amountOut) { _validatePlan(plan); address currentToken = plan.inputToken; uint256 currentAmount = plan.amountIn; if (currentToken == address(0)) { require(nativeValue == currentAmount, "EnhancedSwapRouterV2: native amount mismatch"); IWETH(weth).deposit{value: currentAmount}(); currentToken = weth; } else { IERC20(currentToken).safeTransferFrom(payer, address(this), currentAmount); } for (uint256 i = 0; i < plan.legs.length; i++) { RouteTypesV2.RouteLeg memory leg = plan.legs[i]; if (leg.tokenIn != currentToken) revert InvalidPlan(); if (i == 0 && leg.amountSource != RouteTypesV2.AmountSource.UserInput) revert InvalidPlan(); if (i > 0 && leg.amountSource != RouteTypesV2.AmountSource.PreviousLeg) revert InvalidPlan(); uint8 providerId = uint8(leg.provider); if (!providerEnabled[providerId]) revert ProviderDisabled(); address adapter = providerAdapters[providerId]; if (adapter == address(0)) revert ProviderAdapterNotConfigured(); (bool ok, string memory reason) = IRouteExecutorAdapter(adapter).validate(leg); if (!ok) revert RouteValidationFailed(reason); IERC20(currentToken).safeTransfer(adapter, currentAmount); uint256 balanceBefore = IERC20(leg.tokenOut).balanceOf(address(this)); uint256 reportedAmountOut = IRouteExecutorAdapter(adapter).execute(leg, currentAmount); uint256 balanceAfter = IERC20(leg.tokenOut).balanceOf(address(this)); uint256 actualAmountOut = balanceAfter - balanceBefore; if (actualAmountOut == 0) { actualAmountOut = reportedAmountOut; } if (actualAmountOut < leg.minAmountOut) revert InsufficientOutput(); currentToken = leg.tokenOut; currentAmount = actualAmountOut; } if (currentToken != plan.outputToken) revert InvalidPlan(); if (currentAmount < plan.minAmountOut) revert InsufficientOutput(); address recipient = plan.recipient == address(0) ? payer : plan.recipient; IERC20(currentToken).safeTransfer(recipient, currentAmount); emit RouteExecuted( keccak256(abi.encode(plan.chainId, plan.inputToken, plan.outputToken, plan.amountIn, plan.deadline)), payer, recipient, plan.inputToken == address(0) ? weth : plan.inputToken, currentToken, plan.amountIn, currentAmount ); return currentAmount; } function _getRouteConfig( address tokenIn, address tokenOut, RouteTypesV2.Provider preferredProvider ) internal view returns (ProviderRouteConfig memory) { RouteTypesV2.Provider[] memory providers = _getRoutingProviders(preferredProvider); for (uint256 i = 0; i < providers.length; i++) { uint8 providerId = uint8(providers[i]); ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][providerId]; if (config.enabled && config.target != address(0) && providerEnabled[providerId]) { return ProviderRouteConfig({ target: config.target, providerData: config.providerData, enabled: config.enabled }); } } revert RouteNotConfigured(); } function _getRoutingProviders( RouteTypesV2.Provider preferredProvider ) internal view returns (RouteTypesV2.Provider[] memory providers) { if (providerEnabled[uint8(preferredProvider)]) { providers = new RouteTypesV2.Provider[](1); providers[0] = preferredProvider; return providers; } providers = sizeBasedRouting[0]; if (providers.length == 0) { providers = new RouteTypesV2.Provider[](5); providers[0] = RouteTypesV2.Provider.Dodo; providers[1] = RouteTypesV2.Provider.UniswapV3; providers[2] = RouteTypesV2.Provider.Balancer; providers[3] = RouteTypesV2.Provider.Curve; providers[4] = RouteTypesV2.Provider.OneInch; } } function _validatePlan(RouteTypesV2.RoutePlan memory plan) internal view { if (plan.amountIn == 0 || plan.legs.length == 0) revert InvalidPlan(); if (plan.outputToken == address(0)) revert InvalidPlan(); if (plan.chainId != block.chainid) revert InvalidPlan(); if (plan.deadline < block.timestamp) revert InvalidPlan(); } function _initializeDefaultRouting() internal { sizeBasedRouting[0].push(RouteTypesV2.Provider.Dodo); sizeBasedRouting[0].push(RouteTypesV2.Provider.UniswapV3); sizeBasedRouting[1].push(RouteTypesV2.Provider.Dodo); sizeBasedRouting[1].push(RouteTypesV2.Provider.Balancer); sizeBasedRouting[1].push(RouteTypesV2.Provider.UniswapV3); sizeBasedRouting[2].push(RouteTypesV2.Provider.Dodo); sizeBasedRouting[2].push(RouteTypesV2.Provider.Curve); sizeBasedRouting[2].push(RouteTypesV2.Provider.Balancer); } function _copyPlan( RouteTypesV2.RoutePlan calldata plan ) internal pure returns (RouteTypesV2.RoutePlan memory copied) { copied.chainId = plan.chainId; copied.inputToken = plan.inputToken; copied.outputToken = plan.outputToken; copied.amountIn = plan.amountIn; copied.minAmountOut = plan.minAmountOut; copied.recipient = plan.recipient; copied.deadline = plan.deadline; copied.legs = new RouteTypesV2.RouteLeg[](plan.legs.length); for (uint256 i = 0; i < plan.legs.length; i++) { copied.legs[i] = RouteTypesV2.RouteLeg({ provider: plan.legs[i].provider, tokenIn: plan.legs[i].tokenIn, tokenOut: plan.legs[i].tokenOut, amountSource: plan.legs[i].amountSource, minAmountOut: plan.legs[i].minAmountOut, target: plan.legs[i].target, providerData: plan.legs[i].providerData }); } } function _defaultPreferredProvider() internal pure returns (RouteTypesV2.Provider) { return RouteTypesV2.Provider.Dodo; } function _isValidStablecoin(address token) internal view returns (bool) { return token == usdt || token == usdc || token == dai; } receive() external payable {} }