// 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 "./ChallengeManager.sol"; /** * @title BridgeSwapCoordinator * @notice Coordinates bridge release + swap in single transaction * @dev Verifies claim finalization, releases from liquidity pool, executes swap, transfers stablecoin */ contract BridgeSwapCoordinator is ReentrancyGuard { using SafeERC20 for IERC20; InboxETH public immutable inbox; LiquidityPoolETH public immutable liquidityPool; SwapRouter public immutable swapRouter; ChallengeManager public immutable challengeManager; event BridgeSwapExecuted( uint256 indexed depositId, address indexed recipient, LiquidityPoolETH.AssetType inputAsset, uint256 bridgeAmount, address stablecoinToken, uint256 stablecoinAmount ); error ZeroDepositId(); error ZeroRecipient(); error ClaimNotFinalized(); error ClaimChallenged(); error InsufficientOutput(); /** * @notice Constructor * @param _inbox InboxETH contract address * @param _liquidityPool LiquidityPoolETH contract address * @param _swapRouter SwapRouter contract address * @param _challengeManager ChallengeManager contract address */ constructor( address _inbox, address _liquidityPool, address _swapRouter, address _challengeManager ) { require(_inbox != address(0), "BridgeSwapCoordinator: zero inbox"); require(_liquidityPool != address(0), "BridgeSwapCoordinator: zero liquidity pool"); require(_swapRouter != address(0), "BridgeSwapCoordinator: zero swap router"); require(_challengeManager != address(0), "BridgeSwapCoordinator: zero challenge manager"); inbox = InboxETH(payable(_inbox)); liquidityPool = LiquidityPoolETH(payable(_liquidityPool)); swapRouter = SwapRouter(payable(_swapRouter)); 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 swap (for 1inch) * @return stablecoinAmount Amount of stablecoin received */ function bridgeAndSwap( uint256 depositId, address recipient, LiquidityPoolETH.AssetType outputAsset, address stablecoinToken, uint256 amountOutMin, bytes calldata routeData ) external nonReentrant returns (uint256 stablecoinAmount) { if (depositId == 0) revert ZeroDepositId(); if (recipient == address(0)) revert ZeroRecipient(); // Verify claim is finalized ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId); if (claim.depositId == 0) revert("BridgeSwapCoordinator: claim not found"); if (!claim.finalized) revert ClaimNotFinalized(); if (claim.challenged) revert ClaimChallenged(); if (claim.recipient != recipient) revert("BridgeSwapCoordinator: recipient mismatch"); // Use amount from claim (ChallengeManager has the claim data) uint256 bridgeAmount = claim.amount; // Add pending claim (this should have been done during claim submission, but check anyway) // Note: In production, you'd want to track whether funds have already been released // Release funds from liquidity pool to this contract liquidityPool.releaseToRecipient(depositId, address(this), bridgeAmount, outputAsset); // Execute swap if (outputAsset == LiquidityPoolETH.AssetType.ETH) { // Swap ETH to stablecoin via SwapRouter stablecoinAmount = swapRouter.swapToStablecoin{value: bridgeAmount}( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, routeData ); } else { // WETH case: approve and swap // Get WETH address from liquidity pool address wethAddress = liquidityPool.getWeth(); IERC20 wethToken = IERC20(wethAddress); wethToken.approve(address(swapRouter), bridgeAmount); stablecoinAmount = swapRouter.swapToStablecoin( outputAsset, stablecoinToken, bridgeAmount, amountOutMin, routeData ); } if (stablecoinAmount < amountOutMin) revert InsufficientOutput(); // Transfer stablecoin to recipient IERC20(stablecoinToken).safeTransfer(recipient, stablecoinAmount); // Note: Bond release should be handled separately after finalization // This coordinator only handles bridge release + swap emit BridgeSwapExecuted( depositId, recipient, outputAsset, bridgeAmount, stablecoinToken, stablecoinAmount ); return stablecoinAmount; } /** * @notice Check if claim can be swapped * @param depositId Deposit ID * @return canSwap_ True if claim can be swapped * @return reason Reason if cannot swap */ 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, ""); } // Allow contract to receive ETH receive() external payable {} }