- Resolve stash: merge load_deployment_env path with secure-secrets and CR/LF RPC strip - create-pmm-full-mesh-chain138.sh delegates to sync-chain138-pmm-pools-from-json.sh - env.additions.example: canonical PMM pool defaults (cUSDT/USDT per crosscheck) - Include Chain138 scripts, official mirror deploy scaffolding, and prior staged changes Made-with: Cursor
747 lines
26 KiB
Solidity
747 lines
26 KiB
Solidity
// 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 stablecoinToken,
|
|
uint256 amountIn,
|
|
uint256 amountOutMin
|
|
) internal 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 stablecoinToken,
|
|
uint256 amountIn,
|
|
uint256 amountOutMin
|
|
) 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 {}
|
|
}
|