diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index eb21a540e3..daa243ac7c 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol"; import {UniversalTokenLib} from "./libs/UniversalToken.sol"; import {Admin} from "./Admin.sol"; @@ -14,6 +15,7 @@ import {IFastBridgeRecipient} from "./interfaces/IFastBridgeRecipient.sol"; /// @notice FastBridgeV2 is a contract for bridging tokens across chains. contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { + using BridgeTransactionV2Lib for bytes; using SafeERC20 for IERC20; using UniversalTokenLib for address; @@ -61,17 +63,20 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @inheritdoc IFastBridge - function relay(bytes memory request) external payable { + function relay(bytes calldata request) external payable { + // relay override will validate the request relay({request: request, relayer: msg.sender}); } /// @inheritdoc IFastBridge - function prove(bytes memory request, bytes32 destTxHash) external { + function prove(bytes calldata request, bytes32 destTxHash) external { + request.validateV2(); prove({transactionId: keccak256(request), destTxHash: destTxHash, relayer: msg.sender}); } /// @inheritdoc IFastBridgeV2 - function claim(bytes memory request) external { + function claim(bytes calldata request) external { + // claim override will validate the request claim({request: request, to: address(0)}); } @@ -98,38 +103,31 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @inheritdoc IFastBridge - function refund(bytes memory request) external { + function refund(bytes calldata request) external { + request.validateV2(); bytes32 transactionId = keccak256(request); - - BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request); - BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); - if (hasRole(REFUNDER_ROLE, msg.sender)) { - // Refunder can refund if deadline has passed - if (block.timestamp <= transaction.deadline) revert DeadlineNotExceeded(); - } else { - // Permissionless refund is allowed after REFUND_DELAY - if (block.timestamp <= transaction.deadline + REFUND_DELAY) revert DeadlineNotExceeded(); - } - + uint256 deadline = request.deadline(); + // Permissionless refund is allowed after REFUND_DELAY + if (!hasRole(REFUNDER_ROLE, msg.sender)) deadline += REFUND_DELAY; + if (block.timestamp <= deadline) revert DeadlineNotExceeded(); // Note: this is a storage write $.status = BridgeStatus.REFUNDED; // transfer origin collateral back to original sender - uint256 amount = transaction.originAmount + transaction.originFeeAmount; - address to = transaction.originSender; - address token = transaction.originToken; - + address to = request.originSender(); + address token = request.originToken(); + uint256 amount = request.originAmount() + request.originFeeAmount(); if (token == UniversalTokenLib.ETH_ADDRESS) { Address.sendValue(payable(to), amount); } else { IERC20(token).safeTransfer(to, amount); } - emit BridgeDepositRefunded(transactionId, transaction.originSender, transaction.originToken, amount); + emit BridgeDepositRefunded(transactionId, to, token, amount); } /// @inheritdoc IFastBridge @@ -146,7 +144,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { /// - `callValue` is partially reported as a zero/non-zero flag /// - `callParams` is ignored /// In order to process all kinds of requests use getBridgeTransactionV2 instead. - function getBridgeTransaction(bytes memory request) external view returns (BridgeTransaction memory) { + function getBridgeTransaction(bytes calldata request) external view returns (BridgeTransaction memory) { // Try decoding into V2 struct first. This will revert if V1 struct is passed try this.getBridgeTransactionV2(request) returns (BridgeTransactionV2 memory txV2) { // Note: we entirely ignore the callParams field, as it was not present in V1 @@ -170,6 +168,12 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } } + /// @inheritdoc IFastBridgeV2 + function getBridgeTransactionV2(bytes calldata request) external pure returns (BridgeTransactionV2 memory) { + request.validateV2(); + return BridgeTransactionV2Lib.decodeV2(request); + } + /// @inheritdoc IFastBridgeV2 function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { int256 exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds; @@ -187,7 +191,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } // set status to requested - bytes memory request = abi.encode( + bytes memory request = BridgeTransactionV2Lib.encodeV2( BridgeTransactionV2({ originChainId: uint32(block.chainid), destChainId: params.dstChainId, @@ -198,12 +202,12 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { originAmount: originAmount, destAmount: params.destAmount, originFeeAmount: originFeeAmount, - callValue: paramsV2.callValue, deadline: params.deadline, nonce: senderNonces[params.sender]++, // increment nonce on every bridge exclusivityRelayer: paramsV2.quoteRelayer, // We checked exclusivityEndTime to be in range (0 .. params.deadline] above, so can safely cast exclusivityEndTime: uint256(exclusivityEndTime), + callValue: paramsV2.callValue, callParams: paramsV2.callParams }) ); @@ -225,29 +229,29 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @inheritdoc IFastBridgeV2 - function relay(bytes memory request, address relayer) public payable { + function relay(bytes calldata request, address relayer) public payable { + request.validateV2(); bytes32 transactionId = keccak256(request); - BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request); - _validateRelayParams(transaction, transactionId, relayer); + _validateRelayParams(request, transactionId, relayer); // mark bridge transaction as relayed bridgeRelayDetails[transactionId] = BridgeRelay({blockNumber: uint48(block.number), blockTimestamp: uint48(block.timestamp), relayer: relayer}); // transfer tokens to recipient on destination chain and do an arbitrary call if requested - address to = transaction.destRecipient; - address token = transaction.destToken; - uint256 amount = transaction.destAmount; - uint256 callValue = transaction.callValue; + address to = request.destRecipient(); + address token = request.destToken(); + uint256 amount = request.destAmount(); + uint256 callValue = request.callValue(); // Emit the event before any external calls emit BridgeRelayed({ transactionId: transactionId, relayer: relayer, to: to, - originChainId: transaction.originChainId, - originToken: transaction.originToken, + originChainId: request.originChainId(), + originToken: request.originToken(), destToken: token, - originAmount: transaction.originAmount, + originAmount: request.originAmount(), destAmount: amount, chainGasAmount: callValue }); @@ -267,11 +271,12 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { IERC20(token).safeTransferFrom(msg.sender, to, amount); } - if (transaction.callParams.length != 0) { + bytes calldata callParams = request.callParams(); + if (callParams.length != 0) { // Arbitrary call requested, perform it while supplying full msg.value to the recipient // Note: if token has a fee on transfers, the recipient will have received less than `amount`. // This is a very niche edge case and should be handled by the recipient contract. - _checkedCallRecipient({recipient: to, token: token, amount: amount, callParams: transaction.callParams}); + _checkedCallRecipient({recipient: to, token: token, amount: amount, callParams: callParams}); } else if (msg.value != 0) { // No arbitrary call requested, but msg.value was sent. This is either a relay with ETH, // or a non-zero callValue request with an ERC20. In both cases, transfer the ETH to the recipient. @@ -295,10 +300,9 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @inheritdoc IFastBridge - function claim(bytes memory request, address to) public { + function claim(bytes calldata request, address to) public { + request.validateV2(); bytes32 transactionId = keccak256(request); - BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request); - BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; address proofRelayer = $.proofRelayer; @@ -322,10 +326,10 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { $.status = BridgeStatus.RELAYER_CLAIMED; // update protocol fees if origin fee amount exists - if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount; - - address token = transaction.originToken; - uint256 amount = transaction.originAmount; + address token = request.originToken(); + uint256 amount = request.originAmount(); + uint256 originFeeAmount = request.originFeeAmount(); + if (originFeeAmount > 0) protocolFees[token] += originFeeAmount; // transfer origin collateral to specified address (protocol fee was pre-deducted at deposit) if (token == UniversalTokenLib.ETH_ADDRESS) { @@ -334,7 +338,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { IERC20(token).safeTransfer(to, amount); } - emit BridgeDepositClaimed(transactionId, proofRelayer, to, transaction.originToken, transaction.originAmount); + emit BridgeDepositClaimed(transactionId, proofRelayer, to, token, amount); } function bridgeStatuses(bytes32 transactionId) public view returns (BridgeStatus status) { @@ -354,11 +358,6 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { return bridgeRelayDetails[transactionId].relayer != address(0); } - /// @inheritdoc IFastBridgeV2 - function getBridgeTransactionV2(bytes memory request) public pure returns (BridgeTransactionV2 memory) { - return abi.decode(request, (BridgeTransactionV2)); - } - /// @notice Takes the bridged asset from the user into FastBridgeV2 custody. It will be later /// claimed by the relayer who completed the relay on destination chain, or refunded back to the user, /// should no one complete the relay. @@ -386,7 +385,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { address recipient, address token, uint256 amount, - bytes memory callParams + bytes calldata callParams ) internal { @@ -446,27 +445,16 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @notice Performs all the necessary checks for a relay to happen. - function _validateRelayParams( - BridgeTransactionV2 memory transaction, - bytes32 transactionId, - address relayer - ) - internal - view - { + function _validateRelayParams(bytes calldata request, bytes32 transactionId, address relayer) internal view { if (relayer == address(0)) revert ZeroAddress(); // Check if the transaction has already been relayed if (bridgeRelays(transactionId)) revert TransactionRelayed(); - if (transaction.destChainId != block.chainid) revert ChainIncorrect(); + if (request.destChainId() != block.chainid) revert ChainIncorrect(); // Check the deadline for relay to happen - if (block.timestamp > transaction.deadline) revert DeadlineExceeded(); + if (block.timestamp > request.deadline()) revert DeadlineExceeded(); // Check the exclusivity period, if it is still ongoing - // forgefmt: disable-next-item - if ( - transaction.exclusivityRelayer != address(0) && - transaction.exclusivityRelayer != relayer && - block.timestamp <= transaction.exclusivityEndTime - ) { + address exclRelayer = request.exclusivityRelayer(); + if (exclRelayer != address(0) && exclRelayer != relayer && block.timestamp <= request.exclusivityEndTime()) { revert ExclusivityPeriodNotPassed(); } } diff --git a/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol new file mode 100644 index 0000000000..ccdecf58c6 --- /dev/null +++ b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IFastBridgeV2} from "../interfaces/IFastBridgeV2.sol"; + +// solhint-disable no-inline-assembly +library BridgeTransactionV2Lib { + uint16 internal constant VERSION = 2; + + // Offsets of the fields in the packed BridgeTransactionV2 struct + // uint16 version [000 .. 002) + // uint32 originChainId [002 .. 006) + // uint32 destChainId [006 .. 010) + // address originSender [010 .. 030) + // address destRecipient [030 .. 050) + // address originToken [050 .. 070) + // address destToken [070 .. 090) + // uint256 originAmount [090 .. 122) + // uint256 destAmount [122 .. 154) + // uint256 originFeeAmount [154 .. 186) + // uint256 deadline [186 .. 218) + // uint256 nonce [218 .. 250) + // address exclusivityRelayer [250 .. 270) + // uint256 exclusivityEndTime [270 .. 302) + // uint256 callValue [302 .. 334) + // bytes callParams [334 .. ***) + + // forgefmt: disable-start + uint256 private constant OFFSET_ORIGIN_CHAIN_ID = 2; + uint256 private constant OFFSET_DEST_CHAIN_ID = 6; + uint256 private constant OFFSET_ORIGIN_SENDER = 10; + uint256 private constant OFFSET_DEST_RECIPIENT = 30; + uint256 private constant OFFSET_ORIGIN_TOKEN = 50; + uint256 private constant OFFSET_DEST_TOKEN = 70; + uint256 private constant OFFSET_ORIGIN_AMOUNT = 90; + uint256 private constant OFFSET_DEST_AMOUNT = 122; + uint256 private constant OFFSET_ORIGIN_FEE_AMOUNT = 154; + uint256 private constant OFFSET_DEADLINE = 186; + uint256 private constant OFFSET_NONCE = 218; + uint256 private constant OFFSET_EXCLUSIVITY_RELAYER = 250; + uint256 private constant OFFSET_EXCLUSIVITY_END_TIME = 270; + uint256 private constant OFFSET_CALL_VALUE = 302; + uint256 private constant OFFSET_CALL_PARAMS = 334; + // forgefmt: disable-end + + error BridgeTransactionV2__InvalidEncodedTx(); + error BridgeTransactionV2__UnsupportedVersion(uint16 version); + + /// @notice Validates the encoded transaction to be a tightly packed encoded payload for BridgeTransactionV2. + /// @dev Checks the minimum length and the version, use this function before decoding any of the fields. + function validateV2(bytes calldata encodedTx) internal pure { + // Check the minimum length: must at least include all static fields. + if (encodedTx.length < OFFSET_CALL_PARAMS) revert BridgeTransactionV2__InvalidEncodedTx(); + // Once we validated the length, we can be sure that the version field is present. + uint16 version_ = version(encodedTx); + if (version_ != VERSION) revert BridgeTransactionV2__UnsupportedVersion(version_); + } + + /// @notice Encodes the BridgeTransactionV2 struct by tightly packing the fields. + /// @dev `abi.decode` will not work as a result of the tightly packed fields. Use `decodeV2` to decode instead. + function encodeV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) internal pure returns (bytes memory) { + // We split the encoding into two parts to avoid stack-too-deep error + bytes memory firstPart = abi.encodePacked( + VERSION, + bridgeTx.originChainId, + bridgeTx.destChainId, + bridgeTx.originSender, + bridgeTx.destRecipient, + bridgeTx.originToken, + bridgeTx.destToken, + bridgeTx.originAmount + ); + return abi.encodePacked( + firstPart, + bridgeTx.destAmount, + bridgeTx.originFeeAmount, + // Note: we skip the deprecated `sendChainGas` flag, which was present in BridgeTransaction V1 + bridgeTx.deadline, + bridgeTx.nonce, + // New V2 fields: exclusivity + bridgeTx.exclusivityRelayer, + bridgeTx.exclusivityEndTime, + // New V2 fields: arbitrary call + bridgeTx.callValue, + bridgeTx.callParams + ); + } + + /// @notice Decodes the BridgeTransactionV2 struct from the encoded transaction. + /// @dev Encoded BridgeTransactionV2 struct must be tightly packed. + /// Use `validateV2` before decoding to ensure the encoded transaction is valid. + function decodeV2(bytes calldata encodedTx) + internal + pure + returns (IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) + { + bridgeTx.originChainId = originChainId(encodedTx); + bridgeTx.destChainId = destChainId(encodedTx); + bridgeTx.originSender = originSender(encodedTx); + bridgeTx.destRecipient = destRecipient(encodedTx); + bridgeTx.originToken = originToken(encodedTx); + bridgeTx.destToken = destToken(encodedTx); + bridgeTx.originAmount = originAmount(encodedTx); + bridgeTx.destAmount = destAmount(encodedTx); + bridgeTx.originFeeAmount = originFeeAmount(encodedTx); + bridgeTx.callValue = callValue(encodedTx); + bridgeTx.deadline = deadline(encodedTx); + bridgeTx.nonce = nonce(encodedTx); + bridgeTx.exclusivityRelayer = exclusivityRelayer(encodedTx); + bridgeTx.exclusivityEndTime = exclusivityEndTime(encodedTx); + bridgeTx.callParams = callParams(encodedTx); + } + + /// @notice Extracts the version from the encoded transaction. + function version(bytes calldata encodedTx) internal pure returns (uint16 version_) { + // Load 32 bytes from the start and shift it 240 bits to the right to get the highest 16 bits. + assembly { + version_ := shr(240, calldataload(encodedTx.offset)) + } + } + + /// @notice Extracts the origin chain ID from the encoded transaction. + function originChainId(bytes calldata encodedTx) internal pure returns (uint32 originChainId_) { + // Load 32 bytes from the offset and shift it 224 bits to the right to get the highest 32 bits. + assembly { + originChainId_ := shr(224, calldataload(add(encodedTx.offset, OFFSET_ORIGIN_CHAIN_ID))) + } + } + + /// @notice Extracts the destination chain ID from the encoded transaction. + function destChainId(bytes calldata encodedTx) internal pure returns (uint32 destChainId_) { + // Load 32 bytes from the offset and shift it 224 bits to the right to get the highest 32 bits. + assembly { + destChainId_ := shr(224, calldataload(add(encodedTx.offset, OFFSET_DEST_CHAIN_ID))) + } + } + + /// @notice Extracts the origin sender from the encoded transaction. + function originSender(bytes calldata encodedTx) internal pure returns (address originSender_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + originSender_ := shr(96, calldataload(add(encodedTx.offset, OFFSET_ORIGIN_SENDER))) + } + } + + /// @notice Extracts the destination recipient from the encoded transaction. + function destRecipient(bytes calldata encodedTx) internal pure returns (address destRecipient_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + destRecipient_ := shr(96, calldataload(add(encodedTx.offset, OFFSET_DEST_RECIPIENT))) + } + } + + /// @notice Extracts the origin token from the encoded transaction. + function originToken(bytes calldata encodedTx) internal pure returns (address originToken_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + originToken_ := shr(96, calldataload(add(encodedTx.offset, OFFSET_ORIGIN_TOKEN))) + } + } + + /// @notice Extracts the destination token from the encoded transaction. + function destToken(bytes calldata encodedTx) internal pure returns (address destToken_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + destToken_ := shr(96, calldataload(add(encodedTx.offset, OFFSET_DEST_TOKEN))) + } + } + + /// @notice Extracts the origin amount from the encoded transaction. + function originAmount(bytes calldata encodedTx) internal pure returns (uint256 originAmount_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + originAmount_ := calldataload(add(encodedTx.offset, OFFSET_ORIGIN_AMOUNT)) + } + } + + /// @notice Extracts the destination amount from the encoded transaction. + function destAmount(bytes calldata encodedTx) internal pure returns (uint256 destAmount_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + destAmount_ := calldataload(add(encodedTx.offset, OFFSET_DEST_AMOUNT)) + } + } + + /// @notice Extracts the origin fee amount from the encoded transaction. + function originFeeAmount(bytes calldata encodedTx) internal pure returns (uint256 originFeeAmount_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + originFeeAmount_ := calldataload(add(encodedTx.offset, OFFSET_ORIGIN_FEE_AMOUNT)) + } + } + + /// @notice Extracts the deadline from the encoded transaction. + function deadline(bytes calldata encodedTx) internal pure returns (uint256 deadline_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + deadline_ := calldataload(add(encodedTx.offset, OFFSET_DEADLINE)) + } + } + + /// @notice Extracts the nonce from the encoded transaction. + function nonce(bytes calldata encodedTx) internal pure returns (uint256 nonce_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + nonce_ := calldataload(add(encodedTx.offset, OFFSET_NONCE)) + } + } + + /// @notice Extracts the exclusivity relayer from the encoded transaction. + function exclusivityRelayer(bytes calldata encodedTx) internal pure returns (address exclusivityRelayer_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + exclusivityRelayer_ := shr(96, calldataload(add(encodedTx.offset, OFFSET_EXCLUSIVITY_RELAYER))) + } + } + + /// @notice Extracts the exclusivity end time from the encoded transaction. + function exclusivityEndTime(bytes calldata encodedTx) internal pure returns (uint256 exclusivityEndTime_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + exclusivityEndTime_ := calldataload(add(encodedTx.offset, OFFSET_EXCLUSIVITY_END_TIME)) + } + } + + /// @notice Extracts the call value from the encoded transaction. + function callValue(bytes calldata encodedTx) internal pure returns (uint256 callValue_) { + // Load 32 bytes from the offset. No shift is applied, as we need the full 256 bits. + assembly { + callValue_ := calldataload(add(encodedTx.offset, OFFSET_CALL_VALUE)) + } + } + + /// @notice Extracts the call params from the encoded transaction. + function callParams(bytes calldata encodedTx) internal pure returns (bytes calldata callParams_) { + callParams_ = encodedTx[OFFSET_CALL_PARAMS:]; + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol index f499136cfe..42dac15820 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {FastBridgeV2, FastBridgeV2Test, IFastBridgeV2} from "./FastBridgeV2.t.sol"; // solhint-disable func-name-mixedcase, no-empty-blocks @@ -33,7 +35,7 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test { // ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════ function relay(address caller, uint256 msgValue, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public { - bytes memory request = abi.encode(bridgeTx); + bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTx); vm.prank({msgSender: caller, txOrigin: caller}); fastBridge.relay{value: msgValue}(request); } @@ -46,7 +48,7 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test { ) public { - bytes memory request = abi.encode(bridgeTx); + bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTx); vm.prank({msgSender: caller, txOrigin: caller}); fastBridge.relay{value: msgValue}(request, relayer); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol index 047ad6bec2..5a9a6ae4df 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -418,11 +418,23 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { // ══════════════════════════════════════════════════ REVERTS ══════════════════════════════════════════════════════ - function test_relay_revert_usedRequestV1() public { - bytes memory request = abi.encode(extractV1(tokenTx)); - vm.expectRevert(); + function test_relay_revert_requestV1() public { + // V1 doesn't have any version field + expectRevertUnsupportedVersion(0); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.relay(mockRequestV1); + } + + function test_relay_revert_invalidRequestV2() public { + expectRevertInvalidEncodedTx(); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.relay(invalidRequestV2); + } + + function test_relay_revert_requestV3() public { + expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(request); + fastBridge.relay(mockRequestV3); } function test_relay_revert_chainIncorrect() public { @@ -443,11 +455,23 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); } - function test_relay_withRelayerAddress_revert_usedRequestV1() public { - bytes memory request = abi.encode(extractV1(tokenTx)); - vm.expectRevert(); + function test_relay_withRelayerAddress_revert_requestV1() public { + // V1 doesn't have any version field + expectRevertUnsupportedVersion(0); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.relay(mockRequestV1, relayerB); + } + + function test_relay_withRelayerAddress_revert_invalidRequestV2() public { + expectRevertInvalidEncodedTx(); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.relay(invalidRequestV2, relayerB); + } + + function test_relay_withRelayerAddress_revert_requestV3() public { + expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(request, relayerB); + fastBridge.relay(mockRequestV3, relayerB); } function test_relay_withRelayerAddress_revert_chainIncorrect() public { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol index 21d32876fc..08d3bf65c7 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {FastBridgeV2, FastBridgeV2Test, IFastBridge, IFastBridgeV2} from "./FastBridgeV2.t.sol"; // solhint-disable func-name-mixedcase, ordering @@ -41,17 +43,17 @@ contract FastBridgeV2EncodingTest is FastBridgeV2Test { assertEq(a.exclusivityEndTime, b.exclusivityEndTime); } - function test_getBridgeTransaction(IFastBridge.BridgeTransaction memory bridgeTx) public view { - bytes memory request = abi.encode(bridgeTx); + function test_getBridgeTransaction(IFastBridge.BridgeTransaction memory bridgeTxV1) public view { + bytes memory request = abi.encode(bridgeTxV1); IFastBridge.BridgeTransaction memory decodedTx = fastBridge.getBridgeTransaction(request); - assertEq(decodedTx, bridgeTx); + assertEq(decodedTx, bridgeTxV1); } /// @notice We expect all the V1 fields except for `sendChainGas` to match. /// `sendChainGas` is replaced with `callValue` in V2, therefore we expect non-zero `callValue` /// to match `sendChainGas = true` in V1 function test_getBridgeTransaction_supportsV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public view { - bytes memory request = abi.encode(bridgeTxV2); + bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTxV2); IFastBridge.BridgeTransaction memory decodedTx = fastBridge.getBridgeTransaction(request); IFastBridge.BridgeTransaction memory expectedTx = extractV1(bridgeTxV2); expectedTx.sendChainGas = bridgeTxV2.callValue > 0; @@ -59,13 +61,13 @@ contract FastBridgeV2EncodingTest is FastBridgeV2Test { } function test_getBridgeTransactionV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public view { - bytes memory request = abi.encode(bridgeTxV2); + bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTxV2); IFastBridgeV2.BridgeTransactionV2 memory decodedTxV2 = fastBridge.getBridgeTransactionV2(request); assertEq(decodedTxV2, bridgeTxV2); } - function test_getBridgeTransactionV2_revert_usedRequestV1(IFastBridge.BridgeTransaction memory bridgeTx) public { - bytes memory request = abi.encode(bridgeTx); + function test_getBridgeTransactionV2_revert_usedRequestV1(IFastBridge.BridgeTransaction memory bridgeTxV1) public { + bytes memory request = abi.encode(bridgeTxV1); vm.expectRevert(); fastBridge.getBridgeTransactionV2(request); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol index e8bdb2a8b9..0cb70ed357 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {FastBridgeV2SrcBaseTest} from "./FastBridgeV2.Src.Base.t.sol"; // solhint-disable func-name-mixedcase, ordering @@ -11,7 +13,7 @@ contract FastBridgeV2GasBenchmarkEncodingTest is FastBridgeV2SrcBaseTest { } function test_getBridgeTransactionV2() public view { - bytes memory request = abi.encode(tokenTx); + bytes memory request = BridgeTransactionV2Lib.encodeV2(tokenTx); fastBridge.getBridgeTransactionV2(request); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol index e63f292194..f7a4d66633 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {FastBridgeV2, FastBridgeV2Test, IFastBridge, IFastBridgeV2} from "./FastBridgeV2.t.sol"; // solhint-disable func-name-mixedcase, ordering @@ -72,17 +74,17 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function prove(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, bytes32 destTxHash) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.prove(abi.encode(bridgeTx), destTxHash); + fastBridge.prove(BridgeTransactionV2Lib.encodeV2(bridgeTx), destTxHash); } function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.claim(abi.encode(bridgeTx)); + fastBridge.claim(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, address to) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.claim(abi.encode(bridgeTx), to); + fastBridge.claim(BridgeTransactionV2Lib.encodeV2(bridgeTx), to); } function dispute(address caller, bytes32 txId) public { @@ -92,7 +94,7 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function refund(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.refund(abi.encode(bridgeTx)); + fastBridge.refund(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function test_nonce() public view { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 52315635dd..88dcf0bc0e 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {FastBridgeV2SrcBaseTest, IFastBridge, IFastBridgeV2} from "./FastBridgeV2.Src.Base.t.sol"; // solhint-disable func-name-mixedcase, ordering @@ -36,7 +38,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { emit BridgeRequested({ transactionId: txId, sender: bridgeTx.originSender, - request: abi.encode(bridgeTx), + request: BridgeTransactionV2Lib.encodeV2(bridgeTx), destChainId: bridgeTx.destChainId, originToken: bridgeTx.originToken, destToken: bridgeTx.destToken, @@ -910,4 +912,82 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { vm.expectRevert(StatusIncorrect.selector); refund({caller: refunder, 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.claim(mockRequestV1); + } + + function test_claim_revert_invalidRequestV2() public { + expectRevertInvalidEncodedTx(); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.claim(invalidRequestV2); + } + + function test_claim_revert_requestV3() public { + expectRevertUnsupportedVersion(3); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.claim(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_refund_revert_requestV1() public { + // V1 doesn't have any version field + expectRevertUnsupportedVersion(0); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.refund(mockRequestV1); + } + + function test_refund_revert_invalidRequestV2() public { + expectRevertInvalidEncodedTx(); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.refund(invalidRequestV2); + } + + function test_refund_revert_requestV3() public { + expectRevertUnsupportedVersion(3); + vm.prank({msgSender: relayerA, txOrigin: relayerA}); + fastBridge.refund(mockRequestV3); + } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 1a0c3645a3..7c3581e6ee 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {BridgeTransactionV2Lib} from "../contracts/libs/BridgeTransactionV2.sol"; + import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; // solhint-disable-next-line no-unused-import @@ -44,14 +46,42 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { IFastBridgeV2.BridgeParamsV2 internal tokenParamsV2; IFastBridgeV2.BridgeParamsV2 internal ethParamsV2; + bytes internal mockRequestV1; + bytes internal invalidRequestV2; + bytes internal mockRequestV3; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. function testFastBridgeV2Test() external {} + function createInvalidRequestV2(bytes memory requestV2) public pure returns (bytes memory result) { + // Copy everything but the last byte + result = new bytes(requestV2.length - 1); + for (uint256 i = 0; i < result.length; i++) { + result[i] = requestV2[i]; + } + } + + function createMockRequestV3(bytes memory requestV2) public pure returns (bytes memory result) { + result = new bytes(requestV2.length); + // Set the version to 3 + result[0] = 0x00; + result[1] = 0x03; + // Copy the rest of the request + for (uint256 i = 2; i < result.length; i++) { + result[i] = requestV2[i]; + } + } + function setUp() public virtual { srcToken = new MockERC20("SrcToken", 6); dstToken = new MockERC20("DstToken", 6); createFixtures(); + mockRequestV1 = abi.encode(extractV1(tokenTx)); + // Invalid V2 request is formed before `createFixturesV2` to ensure it's not using callParams + invalidRequestV2 = createInvalidRequestV2(BridgeTransactionV2Lib.encodeV2(tokenTx)); createFixturesV2(); + // Mock V3 request is formed after `createFixturesV2` to ensure it's using callParams if needed + mockRequestV3 = createMockRequestV3(BridgeTransactionV2Lib.encodeV2(ethTx)); fastBridge = deployFastBridge(); configureFastBridge(); mintTokens(); @@ -234,13 +264,23 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { } function getTxId(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public pure returns (bytes32) { - return keccak256(abi.encode(bridgeTx)); + return keccak256(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function expectUnauthorized(address caller, bytes32 role) public { vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, role)); } + function expectRevertInvalidEncodedTx() public { + vm.expectRevert(BridgeTransactionV2Lib.BridgeTransactionV2__InvalidEncodedTx.selector); + } + + function expectRevertUnsupportedVersion(uint16 version) public { + vm.expectRevert( + abi.encodeWithSelector(BridgeTransactionV2Lib.BridgeTransactionV2__UnsupportedVersion.selector, version) + ); + } + function cheatCollectedProtocolFees(address token, uint256 amount) public { stdstore.target(address(fastBridge)).sig("protocolFees(address)").with_key(token).checked_write(amount); } diff --git a/packages/contracts-rfq/test/harnesses/BridgeTransactionV2Harness.sol b/packages/contracts-rfq/test/harnesses/BridgeTransactionV2Harness.sol new file mode 100644 index 0000000000..0d28bc21ce --- /dev/null +++ b/packages/contracts-rfq/test/harnesses/BridgeTransactionV2Harness.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {BridgeTransactionV2Lib, IFastBridgeV2} from "../../contracts/libs/BridgeTransactionV2.sol"; + +contract BridgeTransactionV2Harness { + function encodeV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public pure returns (bytes memory) { + return BridgeTransactionV2Lib.encodeV2(bridgeTx); + } + + function decodeV2(bytes calldata encodedTx) public pure returns (IFastBridgeV2.BridgeTransactionV2 memory) { + return BridgeTransactionV2Lib.decodeV2(encodedTx); + } + + function version(bytes calldata encodedTx) public pure returns (uint16) { + return BridgeTransactionV2Lib.version(encodedTx); + } + + function originChainId(bytes calldata encodedTx) public pure returns (uint32) { + return BridgeTransactionV2Lib.originChainId(encodedTx); + } + + function destChainId(bytes calldata encodedTx) public pure returns (uint32) { + return BridgeTransactionV2Lib.destChainId(encodedTx); + } + + function originSender(bytes calldata encodedTx) public pure returns (address) { + return BridgeTransactionV2Lib.originSender(encodedTx); + } + + function destRecipient(bytes calldata encodedTx) public pure returns (address) { + return BridgeTransactionV2Lib.destRecipient(encodedTx); + } + + function originToken(bytes calldata encodedTx) public pure returns (address) { + return BridgeTransactionV2Lib.originToken(encodedTx); + } + + function destToken(bytes calldata encodedTx) public pure returns (address) { + return BridgeTransactionV2Lib.destToken(encodedTx); + } + + function originAmount(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.originAmount(encodedTx); + } + + function destAmount(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.destAmount(encodedTx); + } + + function originFeeAmount(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.originFeeAmount(encodedTx); + } + + function callValue(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.callValue(encodedTx); + } + + function deadline(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.deadline(encodedTx); + } + + function nonce(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.nonce(encodedTx); + } + + function exclusivityRelayer(bytes calldata encodedTx) public pure returns (address) { + return BridgeTransactionV2Lib.exclusivityRelayer(encodedTx); + } + + function exclusivityEndTime(bytes calldata encodedTx) public pure returns (uint256) { + return BridgeTransactionV2Lib.exclusivityEndTime(encodedTx); + } + + function callParams(bytes calldata encodedTx) public pure returns (bytes calldata) { + return BridgeTransactionV2Lib.callParams(encodedTx); + } +} diff --git a/packages/contracts-rfq/test/libs/BridgeTransactionV2.t.sol b/packages/contracts-rfq/test/libs/BridgeTransactionV2.t.sol new file mode 100644 index 0000000000..ae4d042173 --- /dev/null +++ b/packages/contracts-rfq/test/libs/BridgeTransactionV2.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {BridgeTransactionV2Harness, IFastBridgeV2} from "../harnesses/BridgeTransactionV2Harness.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract BridgeTransactionV2Test is Test { + BridgeTransactionV2Harness internal harness; + + function setUp() public { + harness = new BridgeTransactionV2Harness(); + } + + function assertEq( + IFastBridgeV2.BridgeTransactionV2 memory a, + IFastBridgeV2.BridgeTransactionV2 memory b + ) + public + pure + { + assertEq(a.originChainId, b.originChainId); + assertEq(a.destChainId, b.destChainId); + assertEq(a.originSender, b.originSender); + assertEq(a.destRecipient, b.destRecipient); + assertEq(a.originToken, b.originToken); + assertEq(a.destToken, b.destToken); + assertEq(a.originAmount, b.originAmount); + assertEq(a.destAmount, b.destAmount); + assertEq(a.originFeeAmount, b.originFeeAmount); + assertEq(a.callValue, b.callValue); + assertEq(a.deadline, b.deadline); + assertEq(a.nonce, b.nonce); + assertEq(a.exclusivityRelayer, b.exclusivityRelayer); + assertEq(a.exclusivityEndTime, b.exclusivityEndTime); + assertEq(a.callParams, b.callParams); + } + + function test_roundtrip(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public view { + bytes memory encodedTx = harness.encodeV2(bridgeTx); + assertEq(harness.version(encodedTx), 2); + assertEq(harness.originChainId(encodedTx), bridgeTx.originChainId); + assertEq(harness.destChainId(encodedTx), bridgeTx.destChainId); + assertEq(harness.originSender(encodedTx), bridgeTx.originSender); + assertEq(harness.destRecipient(encodedTx), bridgeTx.destRecipient); + assertEq(harness.originToken(encodedTx), bridgeTx.originToken); + assertEq(harness.destToken(encodedTx), bridgeTx.destToken); + assertEq(harness.originAmount(encodedTx), bridgeTx.originAmount); + assertEq(harness.destAmount(encodedTx), bridgeTx.destAmount); + assertEq(harness.originFeeAmount(encodedTx), bridgeTx.originFeeAmount); + assertEq(harness.callValue(encodedTx), bridgeTx.callValue); + assertEq(harness.deadline(encodedTx), bridgeTx.deadline); + assertEq(harness.nonce(encodedTx), bridgeTx.nonce); + assertEq(harness.exclusivityRelayer(encodedTx), bridgeTx.exclusivityRelayer); + assertEq(harness.exclusivityEndTime(encodedTx), bridgeTx.exclusivityEndTime); + assertEq(harness.callParams(encodedTx), bridgeTx.callParams); + } + + function test_roundtrip_decodeV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public view { + bytes memory encodedTx = harness.encodeV2(bridgeTx); + IFastBridgeV2.BridgeTransactionV2 memory decodedTx = harness.decodeV2(encodedTx); + assertEq(decodedTx, bridgeTx); + } +}