497 lines
19 KiB
Solidity
497 lines
19 KiB
Solidity
// 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.ProviderQuote memory quote = _quoteConfiguredProvider(
|
|
tokenIn, tokenOut, amountIn, RouteTypesV2.Provider(providerId), routeConfig, adapter
|
|
);
|
|
if (quote.executable) {
|
|
temp[count] = quote;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
quotes = new RouteTypesV2.ProviderQuote[](count);
|
|
for (uint256 i = 0; i < count; i++) {
|
|
quotes[i] = temp[i];
|
|
}
|
|
}
|
|
|
|
function quoteConfiguredProvider(
|
|
address tokenIn,
|
|
address tokenOut,
|
|
uint256 amountIn,
|
|
RouteTypesV2.Provider provider
|
|
) external view returns (RouteTypesV2.ProviderQuote memory quote) {
|
|
uint8 providerId = uint8(provider);
|
|
if (!providerEnabled[providerId]) revert ProviderDisabled();
|
|
|
|
address adapter = providerAdapters[providerId];
|
|
if (adapter == address(0)) revert ProviderAdapterNotConfigured();
|
|
|
|
ProviderRouteConfig storage routeConfig = providerRoutes[tokenIn][tokenOut][providerId];
|
|
if (!routeConfig.enabled || routeConfig.target == address(0)) revert RouteNotConfigured();
|
|
|
|
return _quoteConfiguredProvider(tokenIn, tokenOut, amountIn, provider, routeConfig, adapter);
|
|
}
|
|
|
|
function _quoteConfiguredProvider(
|
|
address tokenIn,
|
|
address tokenOut,
|
|
uint256 amountIn,
|
|
RouteTypesV2.Provider provider,
|
|
ProviderRouteConfig storage routeConfig,
|
|
address adapter
|
|
) internal view returns (RouteTypesV2.ProviderQuote memory quote) {
|
|
RouteTypesV2.RouteLeg memory leg = RouteTypesV2.RouteLeg({
|
|
provider: provider,
|
|
tokenIn: tokenIn,
|
|
tokenOut: tokenOut,
|
|
amountSource: RouteTypesV2.AmountSource.UserInput,
|
|
minAmountOut: 0,
|
|
target: routeConfig.target,
|
|
providerData: routeConfig.providerData
|
|
});
|
|
|
|
(bool ok,) = IRouteExecutorAdapter(adapter).validate(leg);
|
|
if (!ok) {
|
|
return RouteTypesV2.ProviderQuote({
|
|
provider: provider,
|
|
target: routeConfig.target,
|
|
amountOut: 0,
|
|
estimatedGas: 0,
|
|
executable: false,
|
|
providerData: routeConfig.providerData
|
|
});
|
|
}
|
|
|
|
(uint256 amountOut, uint256 estimatedGas) = IRouteExecutorAdapter(adapter).quote(leg, amountIn);
|
|
return RouteTypesV2.ProviderQuote({
|
|
provider: provider,
|
|
target: routeConfig.target,
|
|
amountOut: amountOut,
|
|
estimatedGas: estimatedGas,
|
|
executable: amountOut > 0,
|
|
providerData: routeConfig.providerData
|
|
});
|
|
}
|
|
|
|
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 {}
|
|
}
|