packages/contracts-rfq/test/FastBridgeV2.Src.t.sol
// 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);
}
}