// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import "../../../contracts/bridge/trustless/DualRouterBridgeSwapCoordinator.sol"; import "../../../contracts/bridge/trustless/ChallengeManager.sol"; import "../../../contracts/bridge/trustless/BondManager.sol"; import "../../../contracts/bridge/trustless/LiquidityPoolETH.sol"; import "../../../contracts/bridge/trustless/InboxETH.sol"; import "../../../contracts/bridge/trustless/EnhancedSwapRouter.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1000000 ether); } function mint(address to, uint256 amount) external { _mint(to, amount); } } /** * Mock SwapRouter: implements swapToStablecoin(..., bytes) returns (uint256). * Transfers stablecoinToken to msg.sender (caller) for amountIn (1:1 mock). */ contract MockBasicSwapRouter { function swapToStablecoin( LiquidityPoolETH.AssetType, address stablecoinToken, uint256 amountIn, uint256, bytes calldata ) external payable returns (uint256) { if (msg.value > 0) { // Caller sent ETH; in real router it would wrap and swap. We just transfer stablecoin. } uint256 out = amountIn; // 1:1 mock require(IERC20(stablecoinToken).transfer(msg.sender, out), "transfer failed"); return out; } } /** * Mock EnhancedSwapRouter: implements swapToStablecoin(..., SwapProvider) returns (uint256, SwapProvider). */ contract MockEnhancedSwapRouter { function swapToStablecoin( LiquidityPoolETH.AssetType, address stablecoinToken, uint256 amountIn, uint256, EnhancedSwapRouter.SwapProvider ) external payable returns (uint256 amountOut, EnhancedSwapRouter.SwapProvider providerUsed) { uint256 out = amountIn; require(IERC20(stablecoinToken).transfer(msg.sender, out), "transfer failed"); return (out, EnhancedSwapRouter.SwapProvider.UniswapV3); } } contract DualRouterBridgeSwapCoordinatorTest is Test { DualRouterBridgeSwapCoordinator public coordinator; BondManager public bondManager; ChallengeManager public challengeManager; LiquidityPoolETH public liquidityPool; InboxETH public inbox; MockBasicSwapRouter public basicRouter; MockEnhancedSwapRouter public enhancedRouter; MockERC20 public weth; MockERC20 public usdt; address public deployer = address(0xDE01); address public relayer = address(0x2222); address public lpProvider = address(0x3333); address public recipient = address(0x4444); uint256 constant BOND_MULTIPLIER = 11000; uint256 constant MIN_BOND = 1 ether; uint256 constant CHALLENGE_WINDOW = 30 minutes; uint256 constant LP_FEE_BPS = 5; uint256 constant MIN_LIQUIDITY_RATIO_BPS = 11000; function setUp() public { vm.startPrank(deployer); weth = new MockERC20("WETH", "WETH"); usdt = new MockERC20("USDT", "USDT"); bondManager = new BondManager(BOND_MULTIPLIER, MIN_BOND); challengeManager = new ChallengeManager(address(bondManager), CHALLENGE_WINDOW); liquidityPool = new LiquidityPoolETH(address(weth), LP_FEE_BPS, MIN_LIQUIDITY_RATIO_BPS); inbox = new InboxETH(address(bondManager), address(challengeManager), address(liquidityPool)); basicRouter = new MockBasicSwapRouter(); enhancedRouter = new MockEnhancedSwapRouter(); coordinator = new DualRouterBridgeSwapCoordinator( address(inbox), address(liquidityPool), address(basicRouter), address(enhancedRouter), address(challengeManager) ); liquidityPool.authorizeRelease(address(coordinator)); usdt.mint(address(basicRouter), 1000 ether); usdt.mint(address(enhancedRouter), 1000 ether); vm.deal(lpProvider, 1000 ether); vm.deal(relayer, 100 ether); vm.stopPrank(); } function _setupFinalizedClaim(uint256 depositAmount) internal returns (uint256 depositId) { depositId = uint256(keccak256(abi.encodePacked(block.timestamp, recipient, depositAmount))); vm.prank(address(inbox)); liquidityPool.addPendingClaim(depositAmount, LiquidityPoolETH.AssetType.ETH); // Simulate ChallengeManager having a finalized claim (we don't have Inbox.submitClaim in this test, // so we need to push claim state into ChallengeManager - but ChallengeManager is deployed and // we can't easily inject a claim without going through Inbox. So we use a different approach: // use the real flow: deploy Lockbox138, user deposits, relayer submits claim, time passes, finalize. // For unit test we can use vm.mockCall to make getClaim return a finalized claim. vm.mockCall( address(challengeManager), abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId), abi.encode( ChallengeManager.Claim({ depositId: depositId, asset: address(0), recipient: recipient, amount: depositAmount, challengeWindowEnd: block.timestamp, finalized: true, challenged: false }) ) ); return depositId; } function testDeployment() public view { assertEq(address(coordinator.inbox()), address(inbox)); assertEq(address(coordinator.liquidityPool()), address(liquidityPool)); assertEq(address(coordinator.swapRouter()), address(basicRouter)); assertEq(address(coordinator.enhancedSwapRouter()), address(enhancedRouter)); assertEq(address(coordinator.challengeManager()), address(challengeManager)); } function testCanSwapWhenFinalized() public { uint256 depositId = 1; vm.mockCall( address(challengeManager), abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId), abi.encode( ChallengeManager.Claim({ depositId: depositId, asset: address(0), recipient: recipient, amount: 1 ether, challengeWindowEnd: block.timestamp, finalized: true, challenged: false }) ) ); (bool canSwap, string memory reason) = coordinator.canSwap(depositId); assertTrue(canSwap); assertEq(reason, ""); } function testCanSwapWhenNotFinalized() public { uint256 depositId = 2; vm.mockCall( address(challengeManager), abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId), abi.encode( ChallengeManager.Claim({ depositId: depositId, asset: address(0), recipient: recipient, amount: 1 ether, challengeWindowEnd: block.timestamp, finalized: false, challenged: false }) ) ); (bool canSwap, string memory reason) = coordinator.canSwap(depositId); assertFalse(canSwap); assertEq(reason, "Claim not finalized"); } }