synapsecns/sanguine

View on GitHub
packages/contracts-rfq/test/FastBridgeV2.Src.t.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol";
import {TokenNotContract} from "../contracts/libs/Errors.sol";

import {FastBridgeV2SrcBaseTest, IFastBridge, IFastBridgeV2} from "./FastBridgeV2.Src.Base.t.sol";

// solhint-disable func-name-mixedcase, ordering
contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest {
    event BridgeRequested(
        bytes32 indexed transactionId,
        address indexed sender,
        bytes request,
        uint32 destChainId,
        address originToken,
        address destToken,
        uint256 originAmount,
        uint256 destAmount,
        bool sendChainGas
    );

    event BridgeProofProvided(bytes32 indexed transactionId, address indexed relayer, bytes32 transactionHash);

    event BridgeDepositClaimed(
        bytes32 indexed transactionId, address indexed relayer, address indexed to, address token, uint256 amount
    );

    event BridgeProofDisputed(bytes32 indexed transactionId, address indexed relayer);

    event BridgeDepositRefunded(bytes32 indexed transactionId, address indexed to, address token, uint256 amount);

    event BridgeQuoteDetails(bytes32 indexed transactionId, bytes quoteId);

    event DisputePenaltyTimeApplied(address prover, uint256 inactiveUntilTimestamp);

    address public claimTo = makeAddr("Claim To");

    function expectBridgeRequested(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, bytes32 txId) public {
        vm.expectEmit(address(fastBridge));
        emit BridgeRequested({
            transactionId: txId,
            sender: bridgeTx.originSender,
            request: BridgeTransactionV2Lib.encodeV2(bridgeTx),
            destChainId: bridgeTx.destChainId,
            originToken: bridgeTx.originToken,
            destToken: bridgeTx.destToken,
            originAmount: bridgeTx.originAmount,
            destAmount: bridgeTx.destAmount,
            sendChainGas: bridgeTx.zapNative > 0
        });
    }

    function expectBridgeQuoteDetails(bytes32 txId, bytes memory quoteId) public {
        vm.expectEmit(address(fastBridge));
        emit BridgeQuoteDetails({transactionId: txId, quoteId: quoteId});
    }

    function expectBridgeProofProvided(bytes32 txId, address relayer, bytes32 destTxHash) public {
        vm.expectEmit(address(fastBridge));
        emit BridgeProofProvided({transactionId: txId, relayer: relayer, transactionHash: destTxHash});
    }

    function expectBridgeDepositClaimed(
        IFastBridgeV2.BridgeTransactionV2 memory bridgeTx,
        bytes32 txId,
        address relayer,
        address to
    )
        public
    {
        vm.expectEmit(address(fastBridge));
        emit BridgeDepositClaimed({
            transactionId: txId,
            relayer: relayer,
            to: to,
            token: bridgeTx.originToken,
            amount: bridgeTx.originAmount
        });
    }

    function expectBridgeProofDisputed(bytes32 txId, address relayer) public {
        vm.expectEmit(address(fastBridge));
        emit BridgeProofDisputed({transactionId: txId, relayer: relayer});
    }

    function expectBridgeDepositRefunded(IFastBridge.BridgeParams memory bridgeParams, bytes32 txId) public {
        vm.expectEmit(address(fastBridge));
        emit BridgeDepositRefunded({
            transactionId: txId,
            to: bridgeParams.sender,
            token: bridgeParams.originToken,
            amount: bridgeParams.originAmount
        });
    }

    function expectDisputePenaltyTimeApplied(address prover) public {
        uint256 inactiveUntilTimestamp = block.timestamp + DISPUTE_PENALTY_TIME;
        vm.expectEmit(address(fastBridge));
        emit DisputePenaltyTimeApplied(prover, inactiveUntilTimestamp);
    }

    // ══════════════════════════════════════════════════ BRIDGE ═══════════════════════════════════════════════════════

    function checkStatusAndProofAfterBridge(bytes32 txId) public view {
        assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED);
        (
            IFastBridgeV2.BridgeStatus status,
            uint32 destChainId,
            uint16 proverID,
            uint256 proofBlockTimestamp,
            address proofRelayer
        ) = fastBridge.bridgeTxDetails(txId);
        assertEq(status, IFastBridgeV2.BridgeStatus.REQUESTED);
        assertEq(destChainId, DST_CHAIN_ID);
        assertEq(proverID, 0);
        assertEq(proofBlockTimestamp, 0);
        assertEq(proofRelayer, address(0));
        (proofBlockTimestamp, proofRelayer) = fastBridge.bridgeProofs(txId);
        assertEq(proofBlockTimestamp, 0);
        assertEq(proofRelayer, address(0));
    }

    function checkTokenBalancesAfterBridge(address caller) public view {
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(caller), LEFTOVER_BALANCE);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
    }

    function test_bridge_token() public {
        bytes32 txId = getTxId(tokenTx);
        expectBridgeRequested(tokenTx, txId);
        expectBridgeQuoteDetails(txId, tokenParamsV2.quoteId);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 0);
        checkStatusAndProofAfterBridge(txId);
        checkTokenBalancesAfterBridge(userA);
    }

    function test_bridge_token_diffSender() public {
        bytes32 txId = getTxId(tokenTx);
        expectBridgeRequested(tokenTx, txId);
        expectBridgeQuoteDetails(txId, tokenParamsV2.quoteId);
        bridge({caller: userB, msgValue: 0, params: tokenParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 0);
        checkStatusAndProofAfterBridge(txId);
        assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount);
        checkTokenBalancesAfterBridge(userB);
    }

    function checkEthBalancesAfterBridge(address caller) public view {
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(address(caller).balance, LEFTOVER_BALANCE);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
    }

    function test_bridge_eth() public {
        // bridge token first to match the nonce
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 0);
        bytes32 txId = getTxId(ethTx);
        expectBridgeRequested(ethTx, txId);
        expectBridgeQuoteDetails(txId, ethParamsV2.quoteId);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        assertEq(fastBridge.senderNonces(userA), 2);
        assertEq(fastBridge.senderNonces(userB), 0);
        checkStatusAndProofAfterBridge(txId);
        checkEthBalancesAfterBridge(userA);
    }

    function test_bridge_eth_diffSender() public {
        // bridge token first to match the nonce
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 0);
        bytes32 txId = getTxId(ethTx);
        expectBridgeRequested(ethTx, txId);
        expectBridgeQuoteDetails(txId, ethParamsV2.quoteId);
        // bridge for user A as sender, called by userB
        bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams});
        assertEq(fastBridge.senderNonces(userA), 2);
        assertEq(fastBridge.senderNonces(userB), 0);
        checkStatusAndProofAfterBridge(txId);
        assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount);
        checkEthBalancesAfterBridge(userB);
    }

    function test_bridge_userSpecificNonce() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 0);
        // UserB nonce is 0
        ethTx.nonce = 0;
        ethParams.sender = userB;
        ethTx.originSender = userB;
        bytes32 txId = getTxId(ethTx);
        expectBridgeRequested(ethTx, txId);
        expectBridgeQuoteDetails(txId, ethParamsV2.quoteId);
        bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams});
        assertEq(fastBridge.senderNonces(userA), 1);
        assertEq(fastBridge.senderNonces(userB), 1);
        checkStatusAndProofAfterBridge(txId);
        checkEthBalancesAfterBridge(userB);
    }

    function test_bridge_token_revert_approvedZero() public virtual {
        vm.prank(userA);
        srcToken.approve(address(fastBridge), 0);
        vm.expectRevert();
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_token_revert_approvedNotEnough() public virtual {
        vm.prank(userA);
        srcToken.approve(address(fastBridge), tokenParams.originAmount - 1);
        vm.expectRevert();
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_token_revert_nonZeroMsgValue() public {
        vm.expectRevert();
        bridge({caller: userA, msgValue: tokenParams.originAmount, params: tokenParams});
    }

    function test_bridge_eth_revert_lowerMsgValue() public {
        vm.expectRevert(MsgValueIncorrect.selector);
        bridge({caller: userA, msgValue: ethParams.originAmount - 1, params: ethParams});
    }

    function test_bridge_eth_revert_higherMsgValue() public {
        vm.expectRevert(MsgValueIncorrect.selector);
        bridge({caller: userA, msgValue: ethParams.originAmount + 1, params: ethParams});
    }

    function test_bridge_eth_revert_zeroMsgValue() public virtual {
        vm.expectRevert(MsgValueIncorrect.selector);
        bridge({caller: userA, msgValue: 0, params: ethParams});
    }

    function test_bridge_revert_sameDestinationChain() public {
        tokenParams.dstChainId = SRC_CHAIN_ID;
        vm.expectRevert(ChainIncorrect.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroOriginAmount() public {
        tokenParams.originAmount = 0;
        vm.expectRevert(AmountIncorrect.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroDestAmount() public {
        tokenParams.destAmount = 0;
        vm.expectRevert(AmountIncorrect.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroOriginToken() public virtual {
        tokenParams.originToken = address(0);
        vm.expectRevert(ZeroAddress.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroDestToken() public {
        tokenParams.destToken = address(0);
        vm.expectRevert(ZeroAddress.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroSender() public virtual {
        tokenParams.sender = address(0);
        vm.expectRevert(ZeroAddress.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_zeroRecipient() public {
        tokenParams.to = address(0);
        vm.expectRevert(ZeroAddress.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_originTokenNotContract() public {
        tokenParams.originToken = makeAddr("Random EOA");
        vm.expectRevert(TokenNotContract.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    function test_bridge_revert_deadlineTooClose() public {
        tokenParams.deadline = block.timestamp + MIN_DEADLINE - 1;
        vm.expectRevert(DeadlineTooShort.selector);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
    }

    // ═══════════════════════════════════════════════════ PROVE ═══════════════════════════════════════════════════════

    function checkStatusAndProofAfterProve(bytes32 txId, uint16 expectedProverID, address relayer) public view {
        assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
        (
            IFastBridgeV2.BridgeStatus status,
            uint32 destChainId,
            uint16 proverID,
            uint256 proofBlockTimestamp,
            address proofRelayer
        ) = fastBridge.bridgeTxDetails(txId);
        assertEq(status, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
        assertEq(destChainId, DST_CHAIN_ID);
        assertEq(proverID, expectedProverID);
        assertEq(proofBlockTimestamp, block.timestamp);
        assertEq(proofRelayer, relayer);
        (proofBlockTimestamp, proofRelayer) = fastBridge.bridgeProofs(txId);
        assertEq(proofBlockTimestamp, block.timestamp);
        assertEq(proofRelayer, relayer);
    }

    function test_prove_token() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        checkStatusAndProofAfterProve(txId, 1, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_prove_eth() public {
        // bridge token first to match the nonce
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        checkStatusAndProofAfterProve(txId, 1, relayerA);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
    }

    function test_prove_revert_statusNull() public {
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
    }

    function test_prove_revert_statusProved() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerB, bridgeTx: tokenTx, destTxHash: hex"02"});
    }

    function test_prove_revert_statusClaimed() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerB, bridgeTx: tokenTx, destTxHash: hex"02"});
    }

    function test_prove_revert_statusRefunded() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        cancel({caller: canceler, bridgeTx: tokenTx});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
    }

    function test_prove_revert_callerNotProver(address caller) public {
        vm.assume(caller != relayerA && caller != relayerB);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: caller, bridgeTx: tokenTx, destTxHash: hex"01"});
    }

    function test_prove_revert_disputePenaltyTime() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        dispute({caller: guard, txId: txId});
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"02"});
        skip(DISPUTE_PENALTY_TIME - 1);
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"02"});
    }

    // ════════════════════════════════════════ PROVE OTHER RELAYER ════════════════════════════════════════════

    function test_proveOther_token() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_proveOther_eth() public {
        // bridge token first to match the nonce
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
    }

    // relayer self-proving using tx id, which is capable of proving for another & most tests focus on that angle.
    function test_proveOther_self() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerA, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 1, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    // arbitrary non-privileged address can be asserted as the relayer
    function test_proveOther_permless() public {
        bytes32 txId = getTxId(tokenTx);

        bridge({caller: userA, msgValue: 0, params: tokenParams});
        expectBridgeProofProvided({txId: txId, relayer: address(0x1234), destTxHash: hex"01"});
        prove({caller: relayerA, transactionId: txId, destTxHash: hex"01", relayer: address(0x1234)});
        checkStatusAndProofAfterProve(txId, 1, address(0x1234));
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_proveOther_afterDispute() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"01"});
        dispute({caller: guard, txId: txId});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"02"});
        prove({caller: relayerB, relayer: relayerA, transactionId: txId, destTxHash: hex"02"});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_proveOther_reProveAfterDispute() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        expectBridgeProofDisputed(txId, relayerA);
        dispute(guard, txId);
        skip(DISPUTE_PENALTY_TIME);
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"02"});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"02", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        expectBridgeProofDisputed(txId, relayerA);
        dispute(guard, txId);
        skip(DISPUTE_PENALTY_TIME);
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"03"});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"03", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 2, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    // can prove long after relaying as long as status is still good
    function test_proveOther_longDelay() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(10 days);
        expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"});
        prove({caller: relayerA, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        checkStatusAndProofAfterProve(txId, 1, relayerA);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_proveOther_revert_statusProved() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"02", relayer: relayerA});
    }

    function test_proveOther_revert_statusClaimed() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
        skip(CLAIM_DELAY + 1);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"02", relayer: relayerA});
    }

    function test_proveOther_revert_statusRefunded() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        cancel({caller: canceler, bridgeTx: tokenTx});
        vm.expectRevert(StatusIncorrect.selector);
        prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
    }

    function test_proveOther_revert_callerNotAuthed(address caller) public {
        bytes32 txId = getTxId(tokenTx);
        vm.assume(caller != relayerA && caller != relayerB);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: caller, transactionId: txId, destTxHash: hex"01", relayer: relayerA});
    }

    function test_proveOther_revert_disputePenaltyTime() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"01"});
        dispute({caller: guard, txId: txId});
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"02"});
        skip(DISPUTE_PENALTY_TIME - 1);
        vm.expectRevert(ProverNotActive.selector);
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"02"});
    }

    // ═══════════════════════════════════════════════════ CLAIM ═══════════════════════════════════════════════════════

    function checkStatusAndProofAfterClaim(
        bytes32 txId,
        uint16 expectedProverID,
        address relayer,
        uint256 expectedProofTS
    )
        public
        view
    {
        assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.RELAYER_CLAIMED);
        (
            IFastBridgeV2.BridgeStatus status,
            uint32 destChainId,
            uint16 proverID,
            uint256 proofBlockTimestamp,
            address proofRelayer
        ) = fastBridge.bridgeTxDetails(txId);
        assertEq(status, IFastBridgeV2.BridgeStatus.RELAYER_CLAIMED);
        assertEq(destChainId, DST_CHAIN_ID);
        assertEq(proverID, expectedProverID);
        assertEq(proofBlockTimestamp, expectedProofTS);
        assertEq(proofRelayer, relayer);
        (proofBlockTimestamp, proofRelayer) = fastBridge.bridgeProofs(txId);
        assertEq(proofBlockTimestamp, expectedProofTS);
        assertEq(proofRelayer, relayer);
    }

    function checkTokenBalancesAfterClaim(address relayer) public view {
        uint256 expectedProtocolFees = INITIAL_PROTOCOL_FEES_TOKEN + tokenTx.originFeeAmount;
        assertEq(fastBridge.protocolFees(address(srcToken)), expectedProtocolFees);
        assertEq(srcToken.balanceOf(relayer), tokenTx.originAmount);
        assertEq(srcToken.balanceOf(address(fastBridge)), expectedProtocolFees);
    }

    function test_claim_token() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        assertTrue(fastBridge.canClaim(txId, relayerA));
        expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkTokenBalancesAfterClaim(relayerA);
    }

    function test_claim_token_permissionless(address caller) public {
        vm.assume(caller != relayerA);
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: caller, bridgeTx: tokenTx});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkTokenBalancesAfterClaim(relayerA);
    }

    function test_claim_token_permissionless_toZeroAddress(address caller) public {
        vm.assume(caller != relayerA);
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: caller, bridgeTx: tokenTx, to: address(0)});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkTokenBalancesAfterClaim(relayerA);
    }

    function test_claim_token_toDiffAddress() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: claimTo});
        claim({caller: relayerA, bridgeTx: tokenTx, to: claimTo});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        assertEq(srcToken.balanceOf(relayerA), 0);
        checkTokenBalancesAfterClaim(claimTo);
    }

    function test_claim_token_longDelay() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 30 days);
        expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkTokenBalancesAfterClaim(relayerA);
    }

    function checkEthBalancesAfterClaim(address relayer) public view {
        uint256 expectedProtocolFees = INITIAL_PROTOCOL_FEES_ETH + ethTx.originFeeAmount;
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), expectedProtocolFees);
        assertEq(address(relayer).balance, ethTx.originAmount);
        assertEq(address(fastBridge).balance, expectedProtocolFees);
    }

    function test_claim_eth() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        assertTrue(fastBridge.canClaim(txId, relayerA));
        expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: relayerA, bridgeTx: ethTx, to: relayerA});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkEthBalancesAfterClaim(relayerA);
    }

    function test_claim_eth_permissionless(address caller) public {
        vm.assume(caller != relayerA);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: caller, bridgeTx: ethTx});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkEthBalancesAfterClaim(relayerA);
    }

    function test_claim_eth_permissionless_toZeroAddress(address caller) public {
        vm.assume(caller != relayerA);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: caller, bridgeTx: ethTx, to: address(0)});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkEthBalancesAfterClaim(relayerA);
    }

    function test_claim_eth_toDiffAddress() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 1);
        expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: claimTo});
        claim({caller: relayerA, bridgeTx: ethTx, to: claimTo});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkEthBalancesAfterClaim(claimTo);
    }

    function test_claim_eth_longDelay() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"});
        uint256 expectedProofTS = block.timestamp;
        skip(CLAIM_DELAY + 30 days);
        expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA});
        claim({caller: relayerA, bridgeTx: ethTx, to: relayerA});
        checkStatusAndProofAfterClaim(txId, 1, relayerA, expectedProofTS);
        checkEthBalancesAfterClaim(relayerA);
    }

    function test_claim_revert_zeroDelay() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        assertFalse(fastBridge.canClaim(getTxId(tokenTx), relayerA));
        vm.expectRevert(DisputePeriodNotPassed.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    function test_claim_revert_smallerDelay() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY);
        assertFalse(fastBridge.canClaim(getTxId(tokenTx), relayerA));
        vm.expectRevert(DisputePeriodNotPassed.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    function test_claim_revert_callerNotProven(address caller, address to) public {
        vm.assume(caller != relayerA && to != address(0));
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        vm.expectRevert(SenderIncorrect.selector);
        fastBridge.canClaim(getTxId(tokenTx), caller);
        vm.expectRevert(SenderIncorrect.selector);
        claim({caller: caller, bridgeTx: tokenTx, to: to});
    }

    function test_claim_revert_statusNull() public {
        bytes32 txId = getTxId(tokenTx);
        vm.expectRevert(StatusIncorrect.selector);
        fastBridge.canClaim(txId, relayerA);
        vm.expectRevert(StatusIncorrect.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    function test_claim_revert_statusRequested() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        vm.expectRevert(StatusIncorrect.selector);
        fastBridge.canClaim(txId, relayerA);
        vm.expectRevert(StatusIncorrect.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    function test_claim_revert_statusClaimed() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        fastBridge.canClaim(txId, relayerA);
        vm.expectRevert(StatusIncorrect.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    function test_claim_revert_statusRefunded() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        cancel({caller: canceler, bridgeTx: tokenTx});
        vm.expectRevert(StatusIncorrect.selector);
        fastBridge.canClaim(txId, relayerA);
        vm.expectRevert(StatusIncorrect.selector);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
    }

    // ══════════════════════════════════════════════════ DISPUTE ══════════════════════════════════════════════════════

    function checkStatusAndProofAfterDispute(bytes32 txId) public view {
        // Should be identical to a requested tx that was never proven
        checkStatusAndProofAfterBridge(txId);
    }

    function checkProver(address prover, uint16 expectedProverID, uint256 expectedActiveFromTimestamp) public view {
        (uint16 proverID, uint256 activeFromTimestamp) = fastBridge.getProverInfo(prover);
        assertEq(proverID, expectedProverID);
        assertEq(activeFromTimestamp, expectedActiveFromTimestamp);
        address p;
        (p, activeFromTimestamp) = fastBridge.getProverInfoByID(expectedProverID);
        assertEq(p, prover);
        assertEq(activeFromTimestamp, expectedActiveFromTimestamp);
    }

    function test_dispute_token() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        expectDisputePenaltyTimeApplied({prover: relayerA});
        expectBridgeProofDisputed({txId: txId, relayer: relayerA});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerA), 0);
        checkProver(relayerA, 1, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_token_provedOther() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"01"});
        expectDisputePenaltyTimeApplied({prover: relayerA});
        expectBridgeProofDisputed({txId: txId, relayer: relayerB});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerA), 0);
        checkProver(relayerA, 1, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_token_proverAlreadyRemoved() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, relayer: relayerB, transactionId: txId, destTxHash: hex"01"});
        fastBridge.removeProver(relayerA);
        expectBridgeProofDisputed({txId: txId, relayer: relayerB});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerA), 0);
        checkProver(relayerA, 1, 0);
    }

    function test_dispute_token_justBeforeDeadline() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY);
        expectDisputePenaltyTimeApplied({prover: relayerA});
        expectBridgeProofDisputed({txId: txId, relayer: relayerA});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerA), 0);
        checkProver(relayerA, 1, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_eth() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerB, bridgeTx: ethTx, destTxHash: hex"01"});
        expectDisputePenaltyTimeApplied({prover: relayerB});
        expectBridgeProofDisputed({txId: txId, relayer: relayerB});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerB), 0);
        checkProver(relayerB, 2, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_eth_provedOther() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerB, relayer: relayerA, transactionId: txId, destTxHash: hex"01"});
        expectDisputePenaltyTimeApplied({prover: relayerB});
        expectBridgeProofDisputed({txId: txId, relayer: relayerA});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerB), 0);
        checkProver(relayerB, 2, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_eth_proverAlreadyRemoved() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerB, relayer: relayerA, transactionId: txId, destTxHash: hex"01"});
        fastBridge.removeProver(relayerB);
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerB), 0);
        checkProver(relayerB, 2, 0);
    }

    function test_dispute_eth_justBeforeDeadline() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        prove({caller: relayerB, bridgeTx: ethTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY);
        expectDisputePenaltyTimeApplied({prover: relayerB});
        expectBridgeProofDisputed({txId: txId, relayer: relayerB});
        dispute({caller: guard, txId: txId});
        checkStatusAndProofAfterDispute(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount);
        // Check disputed prover
        assertEq(fastBridge.getActiveProverID(relayerB), 0);
        checkProver(relayerB, 2, block.timestamp + DISPUTE_PENALTY_TIME);
    }

    function test_dispute_revert_afterDeadline() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        vm.expectRevert(DisputePeriodPassed.selector);
        dispute({caller: guard, txId: txId});
    }

    function test_dispute_revert_callerNotGuard(address caller) public {
        vm.assume(caller != guard);
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        expectUnauthorized(caller, fastBridge.GUARD_ROLE());
        dispute({caller: caller, txId: txId});
    }

    function test_dispute_revert_statusNull() public {
        bytes32 txId = getTxId(tokenTx);
        vm.expectRevert(StatusIncorrect.selector);
        dispute({caller: guard, txId: txId});
    }

    function test_dispute_revert_statusRequested() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        vm.expectRevert(StatusIncorrect.selector);
        dispute({caller: guard, txId: txId});
    }

    function test_dispute_revert_statusClaimed() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        dispute({caller: guard, txId: txId});
    }

    function test_dispute_revert_statusRefunded() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        cancel({caller: canceler, bridgeTx: tokenTx});
        vm.expectRevert(StatusIncorrect.selector);
        dispute({caller: guard, txId: txId});
    }

    // ══════════════════════════════════════════════════ CANCEL ═══════════════════════════════════════════════════════

    function checkStatusAndProofAfterCancel(bytes32 txId) public view {
        assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REFUNDED);
        (
            IFastBridgeV2.BridgeStatus status,
            uint32 destChainId,
            uint16 proverID,
            uint256 proofBlockTimestamp,
            address proofRelayer
        ) = fastBridge.bridgeTxDetails(txId);
        assertEq(status, IFastBridgeV2.BridgeStatus.REFUNDED);
        assertEq(destChainId, DST_CHAIN_ID);
        assertEq(proverID, 0);
        assertEq(proofBlockTimestamp, 0);
        assertEq(proofRelayer, address(0));
        (proofBlockTimestamp, proofRelayer) = fastBridge.bridgeProofs(txId);
        assertEq(proofBlockTimestamp, 0);
        assertEq(proofRelayer, address(0));
    }

    function test_cancel_token() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId});
        cancel({caller: canceler, bridgeTx: tokenTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    /// @notice Deposit should be refunded to the BridgeParams.sender, regardless of the actual caller
    function test_cancel_token_diffSender() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userB, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId});
        cancel({caller: canceler, bridgeTx: tokenTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + 2 * tokenParams.originAmount);
        assertEq(srcToken.balanceOf(userB), LEFTOVER_BALANCE);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_cancel_token_longDelay() public {
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 30 days);
        expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId});
        cancel({caller: canceler, bridgeTx: tokenTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_cancel_token_permisionless(address caller) public {
        vm.assume(caller != canceler);
        bytes32 txId = getTxId(tokenTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + PERMISSIONLESS_CANCEL_DELAY + 1);
        expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId});
        cancel({caller: caller, bridgeTx: tokenTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN);
        assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount);
        assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN);
    }

    function test_cancel_eth() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        skip(DEADLINE + 1);
        expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId});
        cancel({caller: canceler, bridgeTx: ethTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH);
    }

    /// @notice Deposit should be refunded to the BridgeParams.sender, regardless of the actual caller
    function test_cancel_eth_diffSender() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams});
        skip(DEADLINE + 1);
        expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId});
        cancel({caller: canceler, bridgeTx: ethTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(userA.balance, LEFTOVER_BALANCE + 2 * ethParams.originAmount);
        assertEq(userB.balance, LEFTOVER_BALANCE);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH);
    }

    function test_cancel_eth_longDelay() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        skip(DEADLINE + 30 days);
        expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId});
        cancel({caller: canceler, bridgeTx: ethTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH);
    }

    function test_cancel_eth_permisionless(address caller) public {
        vm.assume(caller != canceler);
        bytes32 txId = getTxId(ethTx);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams});
        skip(DEADLINE + PERMISSIONLESS_CANCEL_DELAY + 1);
        expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId});
        cancel({caller: caller, bridgeTx: ethTx});
        checkStatusAndProofAfterCancel(txId);
        assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH);
        assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount);
        assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH);
    }

    function test_cancel_revert_zeroDelay() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        vm.expectRevert(DeadlineNotExceeded.selector);
        cancel({caller: canceler, bridgeTx: tokenTx});
    }

    function test_cancel_revert_justBeforeDeadline() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE);
        vm.expectRevert(DeadlineNotExceeded.selector);
        cancel({caller: canceler, bridgeTx: tokenTx});
    }

    function test_cancel_revert_justBeforeDeadline_permisionless(address caller) public {
        vm.assume(caller != canceler);
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + PERMISSIONLESS_CANCEL_DELAY);
        vm.expectRevert(DeadlineNotExceeded.selector);
        cancel({caller: caller, bridgeTx: tokenTx});
    }

    function test_cancel_revert_statusNull() public {
        vm.expectRevert(StatusIncorrect.selector);
        cancel({caller: canceler, bridgeTx: ethTx});
    }

    function test_cancel_revert_statusProven() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        vm.expectRevert(StatusIncorrect.selector);
        cancel({caller: canceler, bridgeTx: tokenTx});
    }

    function test_cancel_revert_statusClaimed() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"});
        skip(CLAIM_DELAY + 1);
        claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA});
        vm.expectRevert(StatusIncorrect.selector);
        cancel({caller: canceler, bridgeTx: tokenTx});
    }

    function test_cancel_revert_statusRefunded() public {
        bridge({caller: userA, msgValue: 0, params: tokenParams});
        skip(DEADLINE + 1);
        cancel({caller: canceler, bridgeTx: tokenTx});
        vm.expectRevert(StatusIncorrect.selector);
        cancel({caller: canceler, bridgeTx: tokenTx});
    }

    // ═════════════════════════════════════════════ INVALID PAYLOADS ══════════════════════════════════════════════════

    function test_prove_revert_requestV1() public {
        // V1 doesn't have any version field
        expectRevertUnsupportedVersion(0);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.prove(mockRequestV1, hex"01");
    }

    function test_prove_revert_invalidRequestV2() public {
        expectRevertInvalidEncodedTx();
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.prove(invalidRequestV2, hex"01");
    }

    function test_prove_revert_requestV3() public {
        expectRevertUnsupportedVersion(3);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.prove(mockRequestV3, hex"01");
    }

    function test_claim_revert_requestV1() public {
        // V1 doesn't have any version field
        expectRevertUnsupportedVersion(0);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claimV2(mockRequestV1);
    }

    function test_claim_revert_invalidRequestV2() public {
        expectRevertInvalidEncodedTx();
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claimV2(invalidRequestV2);
    }

    function test_claim_revert_requestV3() public {
        expectRevertUnsupportedVersion(3);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claimV2(mockRequestV3);
    }

    function test_claim_toDiffAddress_revert_requestV1() public {
        // V1 doesn't have any version field
        expectRevertUnsupportedVersion(0);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claim(mockRequestV1, relayerB);
    }

    function test_claim_toDiffAddress_revert_invalidRequestV2() public {
        expectRevertInvalidEncodedTx();
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claim(invalidRequestV2, relayerB);
    }

    function test_claim_toDiffAddress_revert_requestV3() public {
        expectRevertUnsupportedVersion(3);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.claim(mockRequestV3, relayerB);
    }

    function test_cancel_revert_requestV1() public {
        // V1 doesn't have any version field
        expectRevertUnsupportedVersion(0);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.cancelV2(mockRequestV1);
    }

    function test_cancel_revert_invalidRequestV2() public {
        expectRevertInvalidEncodedTx();
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.cancelV2(invalidRequestV2);
    }

    function test_cancel_revert_requestV3() public {
        expectRevertUnsupportedVersion(3);
        vm.prank({msgSender: relayerA, txOrigin: relayerA});
        fastBridge.cancelV2(mockRequestV3);
    }
}