// 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/utils/ReentrancyGuard.sol"; import "./InboxETH.sol"; import "./LiquidityPoolETH.sol"; import "./SwapRouter.sol"; import "./EnhancedSwapRouter.sol"; import "./ChallengeManager.sol"; /** * @title DualRouterBridgeSwapCoordinator * @notice Coordinates bridge release + swap using either basic SwapRouter or EnhancedSwapRouter * @dev Verifies claim finalization, releases from liquidity pool, then swaps via chosen router */ contract DualRouterBridgeSwapCoordinator is ReentrancyGuard { using SafeERC20 for IERC20; InboxETH public immutable inbox; LiquidityPoolETH public immutable liquidityPool; SwapRouter public immutable swapRouter; EnhancedSwapRouter public immutable enhancedSwapRouter; ChallengeManager public immutable challengeManager; event BridgeSwapExecuted( uint256 indexed depositId, address indexed recipient, LiquidityPoolETH.AssetType inputAsset, uint256 bridgeAmount, address stablecoinToken, uint256 stablecoinAmount, bool usedEnhancedRouter ); error ZeroDepositId(); error ZeroRecipient(); error ClaimNotFinalized(); error ClaimChallenged(); error InsufficientOutput(); constructor( address _inbox, address _liquidityPool, address _swapRouter, address _enhancedSwapRouter, address _challengeManager ) { require(_inbox != address(0), "DualRouterBridgeSwapCoordinator: zero inbox"); require(_liquidityPool != address(0), "DualRouterBridgeSwapCoordinator: zero liquidity pool"); require(_swapRouter != address(0), "DualRouterBridgeSwapCoordinator: zero swap router"); require(_enhancedSwapRouter != address(0), "DualRouterBridgeSwapCoordinator: zero enhanced swap router"); require(_challengeManager != address(0), "DualRouterBridgeSwapCoordinator: zero challenge manager"); inbox = InboxETH(payable(_inbox)); liquidityPool = LiquidityPoolETH(payable(_liquidityPool)); swapRouter = SwapRouter(payable(_swapRouter)); enhancedSwapRouter = EnhancedSwapRouter(payable(_enhancedSwapRouter)); challengeManager = ChallengeManager(payable(_challengeManager)); } /** * @notice Execute bridge release + swap to stablecoin * @param depositId Deposit ID * @param recipient Recipient address (should match claim recipient) * @param outputAsset Asset type from bridge (ETH or WETH) * @param stablecoinToken Target stablecoin address (USDT, USDC, or DAI) * @param amountOutMin Minimum stablecoin output (slippage protection) * @param routeData Optional route data for basic SwapRouter (ignored when useEnhancedRouter is true) * @param useEnhancedRouter If true, use EnhancedSwapRouter; otherwise use basic SwapRouter * @return stablecoinAmount Amount of stablecoin received */ function bridgeAndSwap( uint256 depositId, address recipient, LiquidityPoolETH.AssetType outputAsset, address stablecoinToken, uint256 amountOutMin, bytes calldata routeData, bool useEnhancedRouter ) external nonReentrant returns (uint256 stablecoinAmount) { if (depositId == 0) revert ZeroDepositId(); if (recipient == address(0)) revert ZeroRecipient(); ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId); if (claim.depositId == 0) revert("DualRouterBridgeSwapCoordinator: claim not found"); if (!claim.finalized) revert ClaimNotFinalized(); if (claim.challenged) revert ClaimChallenged(); if (claim.recipient != recipient) revert("DualRouterBridgeSwapCoordinator: recipient mismatch"); uint256 bridgeAmount = claim.amount; liquidityPool.releaseToRecipient(depositId, address(this), bridgeAmount, outputAsset); if (useEnhancedRouter) { (stablecoinAmount, ) = _swapViaEnhancedRouter(outputAsset, stablecoinToken, bridgeAmount, amountOutMin); } else { stablecoinAmount = _swapViaBasicRouter(outputAsset, stablecoinToken, bridgeAmount, amountOutMin, routeData); } if (stablecoinAmount < amountOutMin) revert InsufficientOutput(); IERC20(stablecoinToken).safeTransfer(recipient, stablecoinAmount); emit BridgeSwapExecuted( depositId, recipient, outputAsset, bridgeAmount, stablecoinToken, stablecoinAmount, useEnhancedRouter ); return stablecoinAmount; } function _swapViaBasicRouter( LiquidityPoolETH.AssetType outputAsset, address stablecoinToken, uint256 bridgeAmount, uint256 amountOutMin, bytes calldata routeData ) internal returns (uint256) { if (outputAsset == LiquidityPoolETH.AssetType.ETH) { return swapRouter.swapToStablecoin{value: bridgeAmount}( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, routeData ); } address wethAddress = liquidityPool.getWeth(); IERC20(wethAddress).approve(address(swapRouter), bridgeAmount); return swapRouter.swapToStablecoin( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, routeData ); } function _swapViaEnhancedRouter( LiquidityPoolETH.AssetType outputAsset, address stablecoinToken, uint256 bridgeAmount, uint256 amountOutMin ) internal returns (uint256, EnhancedSwapRouter.SwapProvider) { // SwapProvider(0) = UniswapV3; used as "auto" / first in default routing EnhancedSwapRouter.SwapProvider preferred = EnhancedSwapRouter.SwapProvider.UniswapV3; if (outputAsset == LiquidityPoolETH.AssetType.ETH) { return enhancedSwapRouter.swapToStablecoin{value: bridgeAmount}( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, preferred ); } address wethAddress = liquidityPool.getWeth(); IERC20(wethAddress).approve(address(enhancedSwapRouter), bridgeAmount); return enhancedSwapRouter.swapToStablecoin( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, preferred ); } /** * @notice Check if claim can be swapped */ function canSwap(uint256 depositId) external view returns (bool canSwap_, string memory reason) { ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId); if (claim.depositId == 0) return (false, "Claim not found"); if (!claim.finalized) return (false, "Claim not finalized"); if (claim.challenged) return (false, "Claim was challenged"); return (true, ""); } receive() external payable {} }