// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../../../contracts/bridge/trustless/Lockbox138.sol"; import "../../../contracts/bridge/trustless/BondManager.sol"; import "../../../contracts/bridge/trustless/ChallengeManager.sol"; import "../../../contracts/bridge/trustless/LiquidityPoolETH.sol"; import "../../../contracts/bridge/trustless/InboxETH.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockWETH is ERC20 { constructor() ERC20("Wrapped Ether", "WETH") {} function deposit() external payable { _mint(msg.sender, msg.value); } function withdraw(uint256 amount) external { _burn(msg.sender, amount); payable(msg.sender).transfer(amount); } } contract EndToEndTest is Test { // ChainID 138 contracts Lockbox138 public lockbox; // Ethereum contracts BondManager public bondManager; ChallengeManager public challengeManager; LiquidityPoolETH public liquidityPool; InboxETH public inbox; MockWETH public weth; // Test addresses address public user = address(0x1); address public relayer = address(0x2); address public challenger = address(0x3); address public lp = address(0x4); address public recipient = address(0x5); uint256 constant BOND_MULTIPLIER = 11000; // 110% uint256 constant MIN_BOND = 1 ether; uint256 constant CHALLENGE_WINDOW = 30 minutes; uint256 constant LP_FEE_BPS = 5; // 0.05% uint256 constant MIN_LIQUIDITY_RATIO_BPS = 11000; // 110% function setUp() public { // Deploy WETH weth = new MockWETH(); // Deploy Ethereum contracts 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)); // Authorize inbox to manage pending claims liquidityPool.authorizeRelease(address(inbox)); // Deploy ChainID 138 contract lockbox = new Lockbox138(); // Fund addresses vm.deal(user, 100 ether); vm.deal(relayer, 100 ether); vm.deal(challenger, 100 ether); vm.deal(lp, 1000 ether); vm.deal(recipient, 10 ether); // Set initial timestamp to avoid cooldown issues with uninitialized lastClaimTime vm.warp(1000); } function testHappyPath_DepositClaimFinalize() public { uint256 depositAmount = 10 ether; bytes32 nonce = keccak256("test-nonce"); uint256 depositId; // Step 1: User deposits on ChainID 138 vm.prank(user); depositId = lockbox.depositNative{value: depositAmount}(recipient, nonce); // Step 2: Provide liquidity on Ethereum vm.prank(lp); liquidityPool.provideLiquidity{value: 100 ether}(LiquidityPoolETH.AssetType.ETH); // Step 3: Relayer submits claim on Ethereum uint256 requiredBond = bondManager.getRequiredBond(depositAmount); // Advance time to ensure no cooldown issues (first claim doesn't need this, but safe) vm.warp(block.timestamp + 1); vm.prank(relayer); inbox.submitClaim{value: requiredBond}( depositId, address(0), // ETH depositAmount, recipient, "" ); // Step 4: Wait for challenge window to expire vm.warp(block.timestamp + CHALLENGE_WINDOW + 1); // Step 5: Finalize claim challengeManager.finalizeClaim(depositId); // Verify claim is finalized ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId); assertTrue(claim.finalized); assertFalse(claim.challenged); // Step 6: Release bond (would be done by coordinator in production) uint256 relayerBalanceBefore = relayer.balance; bondManager.releaseBond(depositId); // Verify bond released assertEq(relayer.balance, relayerBalanceBefore + requiredBond); // Bond returned } function testChallenge_FraudProof() public { uint256 depositAmount = 10 ether; bytes32 nonce = keccak256("test-nonce-challenge"); uint256 depositId; // Step 1: User deposits vm.prank(user); depositId = lockbox.depositNative{value: depositAmount}(recipient, nonce); // Step 2: Provide liquidity vm.prank(lp); liquidityPool.provideLiquidity{value: 100 ether}(LiquidityPoolETH.AssetType.ETH); // Step 3: Relayer submits fraudulent claim (wrong amount) uint256 fraudulentAmount = depositAmount * 2; // Claim double the amount uint256 requiredBond = bondManager.getRequiredBond(fraudulentAmount); vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: requiredBond}( depositId, address(0), fraudulentAmount, // Wrong amount recipient, "" ); // Step 4: Challenger attempts to challenge the claim (within challenge window) // Note: The fraud proof is invalid, so the challenge should fail bytes memory fraudProof = abi.encode("fraud-proof-data"); vm.prank(challenger); vm.expectRevert(); // Expect revert (InvalidFraudProof) challengeManager.challengeClaim( depositId, ChallengeManager.FraudProofType.IncorrectAmount, fraudProof ); // Since the proof is invalid, the challenge fails and bond is not slashed // This test verifies that invalid proofs are rejected } function testLiquidityPool_WithdrawBlocked() public { uint256 depositAmount = 10 ether; bytes32 nonce = keccak256("test-nonce-lp"); uint256 depositId; // Provide liquidity vm.prank(lp); liquidityPool.provideLiquidity{value: 100 ether}(LiquidityPoolETH.AssetType.ETH); // Submit claim vm.prank(user); depositId = lockbox.depositNative{value: depositAmount}(recipient, nonce); uint256 requiredBond = bondManager.getRequiredBond(depositAmount); vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: requiredBond}( depositId, address(0), depositAmount, recipient, "" ); // Check pool stats before withdrawal (, uint256 pendingBefore, ) = liquidityPool.getPoolStats(LiquidityPoolETH.AssetType.ETH); // With 100 ETH total, 10 ETH pending, we need at least 11 ETH available (110% of 10) // So we can only withdraw up to 89 ETH (100 - 11) // Try to withdraw 90 ETH (should fail) vm.prank(lp); vm.expectRevert(LiquidityPoolETH.WithdrawalBlockedByLiquidityRatio.selector); liquidityPool.withdrawLiquidity(90 ether, LiquidityPoolETH.AssetType.ETH); // Can withdraw smaller amount that maintains ratio (e.g., 10 ether) vm.prank(lp); liquidityPool.withdrawLiquidity(10 ether, LiquidityPoolETH.AssetType.ETH); } function testMultipleConcurrentDeposits() public { // Provide liquidity vm.prank(lp); liquidityPool.provideLiquidity{value: 1000 ether}(LiquidityPoolETH.AssetType.ETH); // Multiple deposits for (uint256 i = 0; i < 5; i++) { uint256 depositAmount = 10 ether; bytes32 nonce = keccak256(abi.encodePacked("nonce-", i)); uint256 depositId; vm.prank(user); depositId = lockbox.depositNative{value: depositAmount}(recipient, nonce); uint256 requiredBond = bondManager.getRequiredBond(depositAmount); vm.deal(relayer, 100 ether); // Advance time to respect cooldown between claims vm.warp(block.timestamp + 61 seconds); vm.prank(relayer); inbox.submitClaim{value: requiredBond}( depositId, address(0), depositAmount, recipient, "" ); } // All claims should be pending (check via getPoolStats) (uint256 total, uint256 pending, ) = liquidityPool.getPoolStats(LiquidityPoolETH.AssetType.ETH); assertEq(pending, 50 ether); } }