// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import "../../../contracts/bridge/trustless/BondManager.sol"; import "../../../contracts/bridge/trustless/ChallengeManager.sol"; import "../../../contracts/bridge/trustless/InboxETH.sol"; import "../../../contracts/bridge/trustless/LiquidityPoolETH.sol"; import "../../../contracts/bridge/trustless/libraries/MerkleProofVerifier.sol"; import "../../../contracts/bridge/trustless/libraries/FraudProofTypes.sol"; /** * @title FraudProofTest * @notice Comprehensive test suite for fraud proof verification */ contract FraudProofTest is Test { BondManager public bondManager; ChallengeManager public challengeManager; InboxETH public inbox; LiquidityPoolETH public liquidityPool; address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); address public relayer = address(0x1111); address public challenger = address(0x2222); address public recipient = address(0x3333); uint256 public constant BOND_MULTIPLIER = 11000; // 110% uint256 public constant MIN_BOND = 1 ether; uint256 public constant CHALLENGE_WINDOW = 30 minutes; uint256 public constant LP_FEE_BPS = 5; // 0.05% uint256 public constant MIN_LIQUIDITY_RATIO_BPS = 11000; // 110% function setUp() public { // Deploy contracts bondManager = new BondManager(BOND_MULTIPLIER, MIN_BOND); challengeManager = new ChallengeManager(address(bondManager), CHALLENGE_WINDOW); liquidityPool = new LiquidityPoolETH(WETH, LP_FEE_BPS, MIN_LIQUIDITY_RATIO_BPS); inbox = new InboxETH(address(bondManager), address(challengeManager), address(liquidityPool)); // Authorize inbox to release from liquidity pool liquidityPool.authorizeRelease(address(inbox)); // Fund relayer and challenger vm.deal(relayer, 100 ether); vm.deal(challenger, 100 ether); // Set initial timestamp to avoid cooldown issues with uninitialized lastClaimTime vm.warp(1000); } function test_NonExistentDepositProof() public { uint256 depositId = 12345; address asset = address(0); // ETH uint256 amount = 1 ether; // Create a fake claim vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: bondManager.getRequiredBond(amount)}( depositId, asset, amount, recipient, "" ); // Create non-existence proof bytes32 stateRoot = keccak256("state_root"); bytes32 depositHash = MerkleProofVerifier.hashDepositData( depositId, asset, amount, recipient, block.timestamp ); bytes32[] memory merkleProof = new bytes32[](2); merkleProof[0] = keccak256("proof1"); merkleProof[1] = keccak256("proof2"); bytes32 leftSibling = keccak256("left"); bytes32 rightSibling = keccak256("right"); bytes memory blockHeader = abi.encodePacked("block_header"); uint256 blockNumber = 1000; FraudProofTypes.NonExistentDepositProof memory proof = FraudProofTypes.NonExistentDepositProof({ stateRoot: stateRoot, depositHash: depositHash, merkleProof: merkleProof, leftSibling: leftSibling, rightSibling: rightSibling, blockHeader: blockHeader, blockNumber: blockNumber }); bytes memory encodedProof = FraudProofTypes.encodeNonExistentDeposit(proof); // Challenge the claim - expect it to fail with InvalidFraudProof since proof is invalid vm.prank(challenger); vm.expectRevert(ChallengeManager.InvalidFraudProof.selector); challengeManager.challengeClaim( depositId, ChallengeManager.FraudProofType.NonExistentDeposit, encodedProof ); } function test_IncorrectAmountProof() public { uint256 depositId = 12346; address asset = address(0); // ETH uint256 claimedAmount = 2 ether; uint256 actualAmount = 1 ether; // Actual amount is less // Create a claim with incorrect amount vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: bondManager.getRequiredBond(claimedAmount)}( depositId, asset, claimedAmount, recipient, "" ); // Create incorrect amount proof bytes32 stateRoot = keccak256("state_root"); bytes32 actualDepositHash = MerkleProofVerifier.hashDepositData( depositId, asset, actualAmount, recipient, block.timestamp ); bytes32[] memory merkleProof = new bytes32[](2); merkleProof[0] = keccak256("proof1"); merkleProof[1] = keccak256("proof2"); bytes memory blockHeader = abi.encodePacked("block_header"); uint256 blockNumber = 1000; FraudProofTypes.IncorrectAmountProof memory proof = FraudProofTypes.IncorrectAmountProof({ stateRoot: stateRoot, depositHash: actualDepositHash, merkleProof: merkleProof, actualAmount: actualAmount, blockHeader: blockHeader, blockNumber: blockNumber }); bytes memory encodedProof = FraudProofTypes.encodeIncorrectAmount(proof); // Challenge the claim - expect it to fail with InvalidFraudProof since proof is invalid vm.prank(challenger); vm.expectRevert(ChallengeManager.InvalidFraudProof.selector); challengeManager.challengeClaim( depositId, ChallengeManager.FraudProofType.IncorrectAmount, encodedProof ); } function test_IncorrectRecipientProof() public { uint256 depositId = 12347; address asset = address(0); // ETH uint256 amount = 1 ether; address actualRecipient = address(0x4444); // Different recipient // Create a claim with incorrect recipient vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: bondManager.getRequiredBond(amount)}( depositId, asset, amount, recipient, // Claimed recipient "" ); // Create incorrect recipient proof bytes32 stateRoot = keccak256("state_root"); bytes32 actualDepositHash = MerkleProofVerifier.hashDepositData( depositId, asset, amount, actualRecipient, block.timestamp ); bytes32[] memory merkleProof = new bytes32[](2); merkleProof[0] = keccak256("proof1"); merkleProof[1] = keccak256("proof2"); bytes memory blockHeader = abi.encodePacked("block_header"); uint256 blockNumber = 1000; FraudProofTypes.IncorrectRecipientProof memory proof = FraudProofTypes.IncorrectRecipientProof({ stateRoot: stateRoot, depositHash: actualDepositHash, merkleProof: merkleProof, actualRecipient: actualRecipient, blockHeader: blockHeader, blockNumber: blockNumber }); bytes memory encodedProof = FraudProofTypes.encodeIncorrectRecipient(proof); // Challenge the claim - expect it to fail with InvalidFraudProof since proof is invalid vm.prank(challenger); vm.expectRevert(ChallengeManager.InvalidFraudProof.selector); challengeManager.challengeClaim( depositId, ChallengeManager.FraudProofType.IncorrectRecipient, encodedProof ); } function test_DoubleSpendProof() public { uint256 depositId = 12348; address asset = address(0); // ETH uint256 amount = 1 ether; // Create first claim vm.warp(block.timestamp + 1); // Advance time vm.prank(relayer); inbox.submitClaim{value: bondManager.getRequiredBond(amount)}( depositId, asset, amount, recipient, "" ); // Finalize first claim vm.warp(block.timestamp + CHALLENGE_WINDOW + 1); challengeManager.finalizeClaim(depositId); // Try to create second claim for same deposit (double spend) - should fail address relayer2 = address(0x5555); vm.deal(relayer2, 100 ether); // Try to create second claim for same deposit (double spend) // This should fail because claim already exists (was finalized) // The check happens at line 132: if (claims[depositId].exists) revert ClaimAlreadyExists(); vm.warp(block.timestamp + 61 seconds); // Advance time for cooldown uint256 requiredBond = bondManager.getRequiredBond(amount); vm.prank(relayer2); vm.expectRevert(InboxETH.ClaimAlreadyExists.selector); inbox.submitClaim{value: requiredBond}( depositId, asset, amount, recipient, "" ); // Note: We cannot challenge a finalized claim, so we skip the challenge part // The test verifies that duplicate claims are rejected at submission time } function test_MerkleProofVerification() public { // Test Merkle proof verification bytes32 root = keccak256("root"); bytes32 leaf = keccak256("leaf"); bytes32[] memory proof = new bytes32[](2); proof[0] = keccak256("proof1"); proof[1] = keccak256("proof2"); // This is a basic test - in production, you'd use actual Merkle tree construction bool isValid = MerkleProofVerifier.verify(proof, root, leaf); // Note: This will fail with random data, but demonstrates the API } function test_FraudProofEncodingDecoding() public { // Test encoding/decoding of fraud proofs bytes32 stateRoot = keccak256("state_root"); bytes32 depositHash = keccak256("deposit_hash"); bytes32[] memory merkleProof = new bytes32[](1); merkleProof[0] = keccak256("proof"); bytes32 leftSibling = keccak256("left"); bytes32 rightSibling = keccak256("right"); bytes memory blockHeader = abi.encodePacked("header"); uint256 blockNumber = 1000; FraudProofTypes.NonExistentDepositProof memory original = FraudProofTypes.NonExistentDepositProof({ stateRoot: stateRoot, depositHash: depositHash, merkleProof: merkleProof, leftSibling: leftSibling, rightSibling: rightSibling, blockHeader: blockHeader, blockNumber: blockNumber }); bytes memory encoded = FraudProofTypes.encodeNonExistentDeposit(original); FraudProofTypes.NonExistentDepositProof memory decoded = FraudProofTypes.decodeNonExistentDeposit(encoded); assertEq(decoded.stateRoot, stateRoot, "State root should match"); assertEq(decoded.depositHash, depositHash, "Deposit hash should match"); assertEq(decoded.blockNumber, blockNumber, "Block number should match"); } }