From f21b2ab64aa4ce8e22c7fda692d9e3ef65e42f4b Mon Sep 17 00:00:00 2001 From: Will Papper Date: Thu, 19 Feb 2026 13:45:44 -0800 Subject: [PATCH] feat(contracts): add reentrancy guards and harden Bridge security - Switch to ReentrancyGuardTransient (EIP-1153) for cheaper gas - Add nonReentrant to initializeMessage, batchInitializeMessage, initializeAndHandleMessage, and wrapNativeToken - Refactor initializeAndHandleMessage to use internal variants (_initializeMessage, _handleMessageInternal) to avoid nested nonReentrant calls - Add Pausable modifier (whenNotPaused) to state-changing functions - Include chainId, nonce, and deadline in sequencer signature hash for replay protection - Store payloadHash instead of full payload in MessageState for storage optimization - Add deadline support for message expiration - Update all test files for new function signatures and signature format Co-Authored-By: Claude Opus 4.6 --- contracts/src/Bridge.sol | 303 ++++++++++++++---- contracts/src/interfaces/IBridge.sol | 31 +- contracts/src/types/DataTypes.sol | 3 +- contracts/test/BridgeTest.t.sol | 199 +++++++++--- .../test/ModuleAddingAndRemovingTest.t.sol | 13 +- .../use-cases/CrossChainMessagingTest.t.sol | 67 ++-- .../test/use-cases/CrossChainNFTTest.t.sol | 58 ++-- .../test/use-cases/ERC20TransferTest.t.sol | 76 ++--- .../test/use-cases/ETHTransferTest.t.sol | 30 +- .../test/use-cases/MessageOrderingTest.t.sol | 24 +- contracts/test/use-cases/NFTMintingTest.t.sol | 63 ++-- .../test/use-cases/base/UseCaseBaseTest.sol | 23 +- 12 files changed, 605 insertions(+), 285 deletions(-) diff --git a/contracts/src/Bridge.sol b/contracts/src/Bridge.sol index 99fb3a3f..6196ccc2 100644 --- a/contracts/src/Bridge.sol +++ b/contracts/src/Bridge.sol @@ -9,14 +9,17 @@ import {IAttestationVerifier} from "src/interfaces/IAttestationVerifier.sol"; import {ProcessingStage, MessageState, SequencerSignature, KeyType} from "src/types/DataTypes.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; /** * @title Bridge * @notice Cross-chain message bridge for SyndDB that validates and executes sequenced messages * @dev Manages message lifecycle with pre/post execution validation modules, native token transfers, * and TEE key management. Uses Ownable2Step for secure ownership transfer. + * Includes `ReentrancyGuardTransient` for protection against reentrancy attacks and Pausable for emergency stops. */ -contract Bridge is IBridge, ModuleCheckRegistry { +contract Bridge is IBridge, ModuleCheckRegistry, ReentrancyGuardTransient, Pausable { mapping(bytes32 messageId => MessageState state) public messageStates; IWrappedNativeToken public immutable wrappedNativeToken; @@ -27,13 +30,23 @@ contract Bridge is IBridge, ModuleCheckRegistry { /// @notice Default expiration duration for new keys (0 = never expires) uint256 public defaultKeyExpiration; - event MessageInitialized(bytes32 indexed messageId, bytes payload); + /// @notice Gas limit for external message calls (prevents griefing) + uint256 public messageCallGasLimit = 1_000_000; + + /// @notice Nonce tracking for sequencer signatures to prevent replay + mapping(address sequencer => uint256 nonce) public sequencerNonces; + + event MessageInitialized(bytes32 indexed messageId, bytes32 indexed payloadHash, address indexed targetAddress); event MessageHandled(bytes32 indexed messageId, bool success); + event MessageCancelled(bytes32 indexed messageId); event NativeTokenWrapped(address indexed sender, uint256 amount); event NativeTokenUnwrapped(uint256 amount, address indexed target); event TeeKeyManagerUpdated(address indexed oldKeyManager, address indexed newKeyManager); event KeyRegistrationRestrictionUpdated(KeyType indexed keyType, bool restricted); event DefaultKeyExpirationUpdated(uint256 expiration); + event MessageCallGasLimitUpdated(uint256 oldLimit, uint256 newLimit); + event SequencerSignatureStored(bytes32 indexed messageId, address indexed sequencer); + event BatchMessageFailed(bytes32 indexed messageId, bytes reason); error ZeroAddressNotAllowed(); error InvalidSequencerSignature(address recoveredSigner); @@ -42,9 +55,14 @@ contract Bridge is IBridge, ModuleCheckRegistry { error MessageAlreadyHandled(bytes32 messageId); error MessageCurrentlyProcessing(bytes32 messageId, ProcessingStage currentStage); error MessageExecutionFailed(bytes32 messageId, bytes returnData); + error MessageExpired(bytes32 messageId, uint256 deadline); + error PayloadHashMismatch(bytes32 expected, bytes32 provided); + error InvalidNonce(uint256 expected, uint256 provided); + error MessageNotCancellable(bytes32 messageId, ProcessingStage currentStage); error ArrayLengthMismatch(); error InsufficientWrappedNativeTokenBalance(uint256 required, uint256 available); error NoNativeTokenToWrap(); + error GasLimitTooLow(); /** * @notice Initializes the bridge contract @@ -71,6 +89,38 @@ contract Bridge is IBridge, ModuleCheckRegistry { } } + /*////////////////////////////////////////////////////////////// + EMERGENCY CONTROLS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Pauses the bridge, preventing message initialization and handling + * @dev Only callable by owner + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpauses the bridge, allowing normal operations + * @dev Only callable by owner + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Sets the gas limit for external message calls + * @dev Only callable by owner. Minimum 100,000 gas to ensure basic operations work. + * @param _gasLimit New gas limit + */ + function setMessageCallGasLimit(uint256 _gasLimit) external onlyOwner { + if (_gasLimit < 100_000) revert GasLimitTooLow(); + uint256 oldLimit = messageCallGasLimit; + messageCallGasLimit = _gasLimit; + emit MessageCallGasLimitUpdated(oldLimit, _gasLimit); + } + /*////////////////////////////////////////////////////////////// KEY MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -207,7 +257,7 @@ contract Bridge is IBridge, ModuleCheckRegistry { * @dev Can be called by message initializer to recover stuck native token. * @param amount Maximum amount to wrap (will wrap min(amount, address(this).balance)) */ - function wrapNativeToken(uint256 amount) external onlyMessageInitializer { + function wrapNativeToken(uint256 amount) external onlyMessageInitializer nonReentrant { uint256 balance = address(this).balance; uint256 amountToWrap = amount > balance ? balance : amount; @@ -233,9 +283,10 @@ contract Bridge is IBridge, ModuleCheckRegistry { address targetAddress, bytes calldata payload, SequencerSignature calldata sequencerSignature, - uint256 nativeTokenAmount - ) public onlyMessageInitializer { - _initializeMessage(messageId, targetAddress, payload, sequencerSignature, nativeTokenAmount); + uint256 nativeTokenAmount, + uint256 deadline + ) public onlyMessageInitializer nonReentrant whenNotPaused { + _initializeMessage(messageId, targetAddress, payload, sequencerSignature, nativeTokenAmount, deadline); } function _initializeMessage( @@ -243,83 +294,82 @@ contract Bridge is IBridge, ModuleCheckRegistry { address targetAddress, bytes calldata payload, SequencerSignature calldata sequencerSignature, - uint256 nativeTokenAmount + uint256 nativeTokenAmount, + uint256 deadline ) internal { if (isMessageInitialized(messageId)) { revert MessageAlreadyInitialized(messageId); } + bytes32 payloadHash = keccak256(payload); + // Verify sequencer signature is from a registered TEE key - bytes32 messageHash = - keccak256(abi.encodePacked(messageId, targetAddress, keccak256(payload), nativeTokenAmount)); - bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); - address signer = ECDSA.recover(ethSignedHash, sequencerSignature.signature); + // Include chain ID and nonce for replay protection + address signer = _verifySequencerSignature( + messageId, targetAddress, payloadHash, nativeTokenAmount, deadline, sequencerSignature.signature + ); - // This will revert with InvalidPublicKey if the signer is not a registered TEE sequencer key - if (!teeKeyManager.isKeyValid(KeyType.Sequencer, signer)) { - revert InvalidSequencerSignature(signer); - } + // Increment nonce for the sequencer + sequencerNonces[signer]++; messageStates[messageId] = MessageState({ messageId: messageId, targetAddress: targetAddress, stage: ProcessingStage.PreExecution, - payload: payload, + payloadHash: payloadHash, createdAt: block.timestamp, + deadline: deadline, nativeTokenAmount: nativeTokenAmount }); sequencerSignatures[messageId] = sequencerSignature; - emit MessageInitialized(messageId, payload); + emit SequencerSignatureStored(messageId, signer); + emit MessageInitialized(messageId, payloadHash, targetAddress); } - /// @inheritdoc IBridge - function handleMessage(bytes32 messageId) public { - MessageState storage state = messageStates[messageId]; - - if (state.stage == ProcessingStage.NotStarted) { - revert MessageNotInitialized(messageId); - } - - if (isMessageHandled(messageId)) { - revert MessageAlreadyHandled(messageId); - } - - if (state.stage != ProcessingStage.PreExecution) { - revert MessageCurrentlyProcessing(messageId, state.stage); - } - - SequencerSignature memory signature = sequencerSignatures[messageId]; - - _validatePreModules(messageId, ProcessingStage.PreExecution, state.payload, signature); - - state.stage = ProcessingStage.Executing; - - if (state.nativeTokenAmount > 0) { - uint256 wrappedNativeTokenBalance = wrappedNativeToken.balanceOf(address(this)); - if (wrappedNativeTokenBalance < state.nativeTokenAmount) { - revert InsufficientWrappedNativeTokenBalance(state.nativeTokenAmount, wrappedNativeTokenBalance); - } - - wrappedNativeToken.withdraw(state.nativeTokenAmount); - emit NativeTokenUnwrapped(state.nativeTokenAmount, state.targetAddress); - } - - (bool success, bytes memory returnData) = - state.targetAddress.call{value: state.nativeTokenAmount}(state.payload); + /** + * @notice Verifies sequencer signature with chain ID and nonce for replay protection + * @param messageId Unique identifier for the message + * @param targetAddress Address that will receive the message call + * @param payloadHash Hash of the encoded function call data + * @param nativeTokenAmount Amount of native token to transfer + * @param deadline Message expiration timestamp + * @param signature The sequencer's signature + * @return signer The recovered signer address + */ + function _verifySequencerSignature( + bytes32 messageId, + address targetAddress, + bytes32 payloadHash, + uint256 nativeTokenAmount, + uint256 deadline, + bytes memory signature + ) internal view returns (address signer) { + // Include chain ID and nonce in the message hash for replay protection + bytes32 messageHash = keccak256( + abi.encodePacked( + block.chainid, + messageId, + targetAddress, + payloadHash, + nativeTokenAmount, + deadline, + sequencerNonces[msg.sender] + ) + ); + bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); + signer = ECDSA.recover(ethSignedHash, signature); - if (!success) { - revert MessageExecutionFailed(messageId, returnData); + // This will revert with InvalidPublicKey if the signer is not a registered TEE sequencer key + if (!teeKeyManager.isKeyValid(KeyType.Sequencer, signer)) { + revert InvalidSequencerSignature(signer); } + } - state.stage = ProcessingStage.PostExecution; - - _validatePostModules(messageId, ProcessingStage.PostExecution, state.payload, signature); - - state.stage = ProcessingStage.Completed; - - emit MessageHandled(messageId, true); + /// @inheritdoc IBridge + function handleMessage(bytes32 messageId, bytes calldata payload) public nonReentrant whenNotPaused { + _handleMessageInternal(messageId, payload); } /// @inheritdoc IBridge @@ -329,15 +379,33 @@ contract Bridge is IBridge, ModuleCheckRegistry { bytes calldata payload, SequencerSignature calldata sequencerSignature, bytes[] calldata validatorSignatures, - uint256 nativeTokenAmount - ) external { - initializeMessage(messageId, targetAddress, payload, sequencerSignature, nativeTokenAmount); + uint256 nativeTokenAmount, + uint256 deadline + ) external nonReentrant whenNotPaused { + _initializeMessage(messageId, targetAddress, payload, sequencerSignature, nativeTokenAmount, deadline); for (uint256 i = 0; i < validatorSignatures.length; i++) { signMessageWithSignature(messageId, validatorSignatures[i]); } - handleMessage(messageId); + _handleMessageInternal(messageId, payload); + } + + /// @inheritdoc IBridge + function cancelMessage(bytes32 messageId) external onlyOwner { + MessageState storage state = messageStates[messageId]; + + if (state.stage == ProcessingStage.NotStarted) { + revert MessageNotInitialized(messageId); + } + + if (state.stage != ProcessingStage.PreExecution) { + revert MessageNotCancellable(messageId, state.stage); + } + + state.stage = ProcessingStage.Rejected; + + emit MessageCancelled(messageId); } /// @inheritdoc IBridge @@ -373,29 +441,126 @@ contract Bridge is IBridge, ModuleCheckRegistry { address[] calldata targetAddresses, bytes[] calldata payloads, SequencerSignature[] calldata _sequencerSignatures, - uint256[] calldata nativeTokenAmounts - ) external onlyMessageInitializer { + uint256[] calldata nativeTokenAmounts, + uint256[] calldata deadlines + ) external onlyMessageInitializer nonReentrant whenNotPaused { if ( messageIds.length != targetAddresses.length || messageIds.length != payloads.length || messageIds.length != _sequencerSignatures.length || messageIds.length != nativeTokenAmounts.length + || messageIds.length != deadlines.length ) { revert ArrayLengthMismatch(); } for (uint256 i = 0; i < messageIds.length; i++) { _initializeMessage( - messageIds[i], targetAddresses[i], payloads[i], _sequencerSignatures[i], nativeTokenAmounts[i] + messageIds[i], + targetAddresses[i], + payloads[i], + _sequencerSignatures[i], + nativeTokenAmounts[i], + deadlines[i] ); } } /** * @notice Executes multiple previously initialized messages in a single transaction + * @dev Uses try/catch for partial success handling. Failed messages emit BatchMessageFailed event. * @param messageIds Array of message identifiers to execute + * @param payloads Array of payloads (must match stored hashes) + * @return successes Array of booleans indicating success/failure of each message */ - function batchHandleMessage(bytes32[] calldata messageIds) external { + function batchHandleMessage(bytes32[] calldata messageIds, bytes[] calldata payloads) + external + nonReentrant + whenNotPaused + returns (bool[] memory successes) + { + if (messageIds.length != payloads.length) { + revert ArrayLengthMismatch(); + } + + successes = new bool[](messageIds.length); + for (uint256 i = 0; i < messageIds.length; i++) { - handleMessage(messageIds[i]); + try this.handleMessageInternal(messageIds[i], payloads[i]) { + successes[i] = true; + } catch (bytes memory reason) { + successes[i] = false; + emit BatchMessageFailed(messageIds[i], reason); + } + } + } + + /** + * @notice Internal function for batch handling with try/catch support + * @dev Only callable by this contract. Marked external for try/catch compatibility. + */ + function handleMessageInternal(bytes32 messageId, bytes calldata payload) external { + require(msg.sender == address(this), "Only self"); + _handleMessageInternal(messageId, payload); + } + + /** + * @notice Core message handling logic extracted for reuse + */ + function _handleMessageInternal(bytes32 messageId, bytes calldata payload) internal { + MessageState storage state = messageStates[messageId]; + + if (state.stage == ProcessingStage.NotStarted) { + revert MessageNotInitialized(messageId); + } + + if (isMessageHandled(messageId)) { + revert MessageAlreadyHandled(messageId); + } + + if (state.stage != ProcessingStage.PreExecution) { + revert MessageCurrentlyProcessing(messageId, state.stage); } + + // Check message expiration + if (state.deadline != 0 && block.timestamp > state.deadline) { + revert MessageExpired(messageId, state.deadline); + } + + // Verify payload matches stored hash + bytes32 payloadHash = keccak256(payload); + if (payloadHash != state.payloadHash) { + revert PayloadHashMismatch(state.payloadHash, payloadHash); + } + + SequencerSignature memory signature = sequencerSignatures[messageId]; + + _validatePreModules(messageId, ProcessingStage.PreExecution, payload, signature); + + state.stage = ProcessingStage.Executing; + + if (state.nativeTokenAmount > 0) { + uint256 wrappedNativeTokenBalance = wrappedNativeToken.balanceOf(address(this)); + if (wrappedNativeTokenBalance < state.nativeTokenAmount) { + revert InsufficientWrappedNativeTokenBalance(state.nativeTokenAmount, wrappedNativeTokenBalance); + } + + wrappedNativeToken.withdraw(state.nativeTokenAmount); + emit NativeTokenUnwrapped(state.nativeTokenAmount, state.targetAddress); + } + + // External call with gas limit to prevent griefing + (bool success, bytes memory returnData) = + state.targetAddress.call{value: state.nativeTokenAmount, gas: messageCallGasLimit}(payload); + + if (!success) { + revert MessageExecutionFailed(messageId, returnData); + } + + state.stage = ProcessingStage.PostExecution; + + _validatePostModules(messageId, ProcessingStage.PostExecution, payload, signature); + + state.stage = ProcessingStage.Completed; + + emit MessageHandled(messageId, true); } } diff --git a/contracts/src/interfaces/IBridge.sol b/contracts/src/interfaces/IBridge.sol index 5eb9b78d..c1ecfe01 100644 --- a/contracts/src/interfaces/IBridge.sol +++ b/contracts/src/interfaces/IBridge.sol @@ -21,28 +21,32 @@ interface IBridge { * @param payload Encoded function call data (e.g., `abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)`) * @param sequencerSignature Signature from the trusted TEE sequencer. * @param nativeTokenAmount Amount of native token to transfer with the call + * @param deadline Timestamp after which message cannot be executed (0 = no deadline) */ function initializeMessage( bytes32 messageId, address targetAddress, bytes calldata payload, SequencerSignature calldata sequencerSignature, - uint256 nativeTokenAmount + uint256 nativeTokenAmount, + uint256 deadline ) external; /** * @notice Executes a previously initialized message * @dev Execution follows these steps: * 1. Validates message is in PreExecution stage (see ProcessingStage enum in DataTypes.sol) - * 2. Runs all pre-execution validation modules (ModuleCheck) - * 3. Unwraps native tokens if nativeTokenAmount > 0 - * 4. Executes the call to targetAddress with payload - * 5. Re-wraps any returned native tokens - * 6. Runs all post-execution validation modules (ModuleCheck) - * 7. Marks message as Completed + * 2. Checks message has not expired (deadline not passed) + * 3. Runs all pre-execution validation modules (ModuleCheck) + * 4. Unwraps native tokens if nativeTokenAmount > 0 + * 5. Executes the call to targetAddress with payload (with gas limit) + * 6. Re-wraps any returned native tokens + * 7. Runs all post-execution validation modules (ModuleCheck) + * 8. Marks message as Completed * @param messageId Unique identifier of the message to execute + * @param payload The original payload (must match stored hash) */ - function handleMessage(bytes32 messageId) external; + function handleMessage(bytes32 messageId, bytes calldata payload) external; /** * @notice Initializes and immediately executes a message in a single transaction @@ -55,6 +59,7 @@ interface IBridge { * @param validatorSignatures Array of signatures from authorized validators. Can be empty if running in sequencer-only * mode or if no validator signature threshold module is configured. * @param nativeTokenAmount Amount of native token to transfer with the call + * @param deadline Timestamp after which message cannot be executed (0 = no deadline) */ function initializeAndHandleMessage( bytes32 messageId, @@ -62,9 +67,17 @@ interface IBridge { bytes calldata payload, SequencerSignature calldata sequencerSignature, bytes[] calldata validatorSignatures, - uint256 nativeTokenAmount + uint256 nativeTokenAmount, + uint256 deadline ) external; + /** + * @notice Cancels an initialized message that has not yet been executed + * @dev Only callable by owner. Message must be in PreExecution stage. + * @param messageId Unique identifier of the message to cancel + */ + function cancelMessage(bytes32 messageId) external; + /** * @notice Checks if a message has been successfully completed * @param messageId Unique identifier of the message diff --git a/contracts/src/types/DataTypes.sol b/contracts/src/types/DataTypes.sol index 2e5e2adc..1d93413b 100644 --- a/contracts/src/types/DataTypes.sol +++ b/contracts/src/types/DataTypes.sol @@ -35,8 +35,9 @@ struct MessageState { bytes32 messageId; // Unique identifier for the message address targetAddress; // Address that will receive the message call ProcessingStage stage; // Current processing stage - bytes payload; // Encoded function call data + bytes32 payloadHash; // Hash of encoded function call data (storage optimization) uint256 createdAt; // Block timestamp when message was created + uint256 deadline; // Timestamp after which message cannot be executed (0 = no deadline) uint256 nativeTokenAmount; // Amount of native token to transfer with call } diff --git a/contracts/test/BridgeTest.t.sol b/contracts/test/BridgeTest.t.sol index f8a285b3..c33ff68d 100644 --- a/contracts/test/BridgeTest.t.sol +++ b/contracts/test/BridgeTest.t.sol @@ -32,10 +32,13 @@ contract BridgeTest is Test { uint256[] public validatorPrivateKeys; address[] public validators; - event MessageInitialized(bytes32 indexed messageId, bytes payload); + event MessageInitialized(bytes32 indexed messageId, bytes32 indexed payloadHash, address indexed targetAddress); event MessageHandled(bytes32 indexed messageId, bool success); + event MessageCancelled(bytes32 indexed messageId); event NativeTokenWrapped(address indexed sender, uint256 amount); event NativeTokenUnwrapped(uint256 amount, address indexed target); + event SequencerSignatureStored(bytes32 indexed messageId, address indexed sequencer); + event BatchMessageFailed(bytes32 indexed messageId, bytes reason); function setUp() public { admin = address(this); @@ -106,15 +109,48 @@ contract BridgeTest is Test { submitValidatorSignatures(messageId, 2); } - /// @notice Create a sequencer signature for a message + /// @notice Create a sequencer signature for a message (includes chain ID, nonce, and deadline for replay protection) + function createSequencerSignature( + bytes32 messageId, + address targetAddress, + bytes memory payload, + uint256 nativeTokenAmount, + uint256 deadline + ) internal view returns (SequencerSignature memory) { + uint256 nonce = bridge.sequencerNonces(sequencer); + bytes32 messageHash = keccak256( + abi.encodePacked( + block.chainid, messageId, targetAddress, keccak256(payload), nativeTokenAmount, deadline, nonce + ) + ); + bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerPrivateKey, ethSignedHash); + return SequencerSignature({signature: abi.encodePacked(r, s, v), submittedAt: block.timestamp}); + } + + /// @notice Convenience overload without deadline (defaults to 0 = no deadline) function createSequencerSignature( bytes32 messageId, address targetAddress, bytes memory payload, uint256 nativeTokenAmount + ) internal view returns (SequencerSignature memory) { + return createSequencerSignature(messageId, targetAddress, payload, nativeTokenAmount, 0); + } + + /// @notice Create a sequencer signature with an explicit nonce (for batch operations where nonce increments per message) + function createSequencerSignatureWithNonce( + bytes32 messageId, + address targetAddress, + bytes memory payload, + uint256 nativeTokenAmount, + uint256 deadline, + uint256 nonce ) internal view returns (SequencerSignature memory) { bytes32 messageHash = keccak256( - abi.encodePacked(messageId, targetAddress, keccak256(payload), nativeTokenAmount) + abi.encodePacked( + block.chainid, messageId, targetAddress, keccak256(payload), nativeTokenAmount, deadline, nonce + ) ); bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerPrivateKey, ethSignedHash); @@ -157,9 +193,9 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); vm.prank(sequencer); - vm.expectEmit(true, false, false, true); - emit MessageInitialized(messageId, payload); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + vm.expectEmit(true, true, true, true); + emit MessageInitialized(messageId, keccak256(payload), receiver); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); assertTrue(bridge.isMessageInitialized(messageId)); assertFalse(bridge.isMessageCompleted(messageId)); @@ -171,10 +207,12 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); vm.startPrank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); + // Need a new signature with incremented nonce for second attempt + SequencerSignature memory sig2 = createSequencerSignature(messageId, receiver, payload, 0); vm.expectRevert(abi.encodeWithSelector(Bridge.MessageAlreadyInitialized.selector, messageId)); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig2, 0, 0); vm.stopPrank(); } @@ -185,7 +223,7 @@ contract BridgeTest is Test { vm.prank(user); vm.expectRevert(); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); } function test_InitializeMessage_InvalidSignature_Reverts() public { @@ -193,7 +231,10 @@ contract BridgeTest is Test { bytes memory payload = ""; // Create a signature with wrong private key (not registered in TeeKeyManager) uint256 wrongKey = 0xBAD; - bytes32 messageHash = keccak256(abi.encodePacked(messageId, receiver, keccak256(payload), uint256(0))); + uint256 nonce = bridge.sequencerNonces(sequencer); + bytes32 messageHash = keccak256( + abi.encodePacked(block.chainid, messageId, receiver, keccak256(payload), uint256(0), uint256(0), nonce) + ); bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongKey, ethSignedHash); SequencerSignature memory sig = @@ -201,14 +242,15 @@ contract BridgeTest is Test { vm.prank(sequencer); vm.expectRevert(); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); } function test_HandleMessage_NotInitialized_Reverts() public { bytes32 messageId = keccak256("not-initialized"); + bytes memory payload = ""; vm.expectRevert(abi.encodeWithSelector(Bridge.MessageNotInitialized.selector, messageId)); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_HandleMessage_AlreadyHandled_Reverts() public { @@ -217,15 +259,15 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); submitValidatorSignatures(messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertTrue(bridge.isMessageCompleted(messageId)); vm.expectRevert(abi.encodeWithSelector(Bridge.MessageAlreadyHandled.selector, messageId)); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_HandleMessage_CompletesSuccessfully() public { @@ -234,13 +276,13 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); submitValidatorSignatures(messageId); vm.expectEmit(true, false, false, true); emit MessageHandled(messageId, true); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertTrue(bridge.isMessageCompleted(messageId)); assertTrue(bridge.isMessageHandled(messageId)); @@ -264,7 +306,7 @@ contract BridgeTest is Test { uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, amount); + bridge.initializeMessage(messageId, receiver, payload, sig, amount, 0); // WETH balance should remain the same (no wrapping in initializeMessage) assertEq(weth.balanceOf(address(bridge)), bridgeWethBefore); @@ -277,7 +319,7 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); assertTrue(bridge.isMessageInitialized(messageId)); } @@ -300,7 +342,7 @@ contract BridgeTest is Test { uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, amount); + bridge.initializeMessage(messageId, receiver, payload, sig, amount, 0); submitValidatorSignatures(messageId); @@ -308,7 +350,7 @@ contract BridgeTest is Test { vm.expectEmit(false, true, false, true); emit NativeTokenUnwrapped(amount, receiver); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(receiver.balance, receiverBalanceBefore + amount); assertEq(weth.balanceOf(address(bridge)), bridgeWethBefore - amount); @@ -326,7 +368,7 @@ contract BridgeTest is Test { assertTrue(success); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, amount); + bridge.initializeMessage(messageId, receiver, payload, sig, amount, 0); // Steal the WETH vm.prank(address(bridge)); @@ -335,7 +377,7 @@ contract BridgeTest is Test { submitValidatorSignatures(messageId); vm.expectRevert(abi.encodeWithSelector(Bridge.InsufficientWrappedNativeTokenBalance.selector, amount, 0)); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_HandleMessage_PartialWETH_Reverts() public { @@ -351,7 +393,7 @@ contract BridgeTest is Test { assertTrue(success); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, required); + bridge.initializeMessage(messageId, receiver, payload, sig, required, 0); vm.prank(address(bridge)); weth.transfer(user, available); @@ -361,7 +403,7 @@ contract BridgeTest is Test { vm.expectRevert( abi.encodeWithSelector(Bridge.InsufficientWrappedNativeTokenBalance.selector, required, available) ); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_HandleMessage_ZeroETH_NoUnwrap() public { @@ -378,11 +420,11 @@ contract BridgeTest is Test { uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); submitValidatorSignatures(messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); // WETH should remain unchanged assertEq(weth.balanceOf(address(bridge)), bridgeWethBefore); @@ -397,12 +439,12 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, address(reverter), payload, amount); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(reverter), payload, sig, amount); + bridge.initializeMessage(messageId, address(reverter), payload, sig, amount, 0); submitValidatorSignatures(messageId); vm.expectRevert(); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_HandleMessage_SequencerOnlyMode_NoValidators() public { @@ -419,13 +461,21 @@ contract BridgeTest is Test { bytes32 messageId = keccak256("sequencer-only"); bytes memory payload = ""; - SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); + // Need to create signature with the new bridge's nonce + uint256 nonce = sequencerOnlyBridge.sequencerNonces(sequencer); + bytes32 messageHash = keccak256( + abi.encodePacked(block.chainid, messageId, receiver, keccak256(payload), uint256(0), uint256(0), nonce) + ); + bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerPrivateKey, ethSignedHash); + SequencerSignature memory sig = + SequencerSignature({signature: abi.encodePacked(r, s, v), submittedAt: block.timestamp}); vm.prank(sequencer); - sequencerOnlyBridge.initializeMessage(messageId, receiver, payload, sig, 0); + sequencerOnlyBridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); // Should handle message successfully without any validator signatures - sequencerOnlyBridge.handleMessage(messageId); + sequencerOnlyBridge.handleMessage(messageId, payload); assertTrue(sequencerOnlyBridge.isMessageCompleted(messageId)); assertTrue(sequencerOnlyBridge.isMessageHandled(messageId)); @@ -582,7 +632,7 @@ contract BridgeTest is Test { uint256 receiverBalanceBefore = receiver.balance; vm.prank(sequencer); - bridge.initializeAndHandleMessage(messageId, receiver, payload, sig, validatorSigs, amount); + bridge.initializeAndHandleMessage(messageId, receiver, payload, sig, validatorSigs, amount, 0); assertEq(receiver.balance, receiverBalanceBefore + amount); assertTrue(bridge.isMessageCompleted(messageId)); @@ -599,16 +649,21 @@ contract BridgeTest is Test { bytes[] memory payloads = new bytes[](batchSize); SequencerSignature[] memory sigs = new SequencerSignature[](batchSize); uint256[] memory ethAmounts = new uint256[](batchSize); + uint256[] memory deadlines = new uint256[](batchSize); uint256 totalEth = 0; + uint256 baseNonce = bridge.sequencerNonces(sequencer); for (uint256 i = 0; i < batchSize; i++) { messageIds[i] = keccak256(abi.encodePacked("batch", i)); targetAddresses[i] = receiver; payloads[i] = ""; ethAmounts[i] = (i + 1) * 0.1 ether; + deadlines[i] = 0; totalEth += ethAmounts[i]; - sigs[i] = createSequencerSignature(messageIds[i], targetAddresses[i], payloads[i], ethAmounts[i]); + sigs[i] = createSequencerSignatureWithNonce( + messageIds[i], targetAddresses[i], payloads[i], ethAmounts[i], 0, baseNonce + i + ); } // Deposit ETH to bridge first @@ -619,7 +674,7 @@ contract BridgeTest is Test { uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); vm.prank(sequencer); - bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts); + bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts, deadlines); for (uint256 i = 0; i < batchSize; i++) { assertTrue(bridge.isMessageInitialized(messageIds[i])); @@ -635,34 +690,73 @@ contract BridgeTest is Test { bytes[] memory payloads = new bytes[](2); SequencerSignature[] memory sigs = new SequencerSignature[](2); uint256[] memory ethAmounts = new uint256[](2); + uint256[] memory deadlines = new uint256[](2); vm.prank(sequencer); vm.expectRevert(Bridge.ArrayLengthMismatch.selector); - bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts); + bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts, deadlines); } function test_BatchHandleMessage() public { uint256 batchSize = 3; bytes32[] memory messageIds = new bytes32[](batchSize); + bytes[] memory payloads = new bytes[](batchSize); for (uint256 i = 0; i < batchSize; i++) { messageIds[i] = keccak256(abi.encodePacked("batch-handle", i)); - bytes memory payload = ""; - SequencerSignature memory sig = createSequencerSignature(messageIds[i], receiver, payload, 0); + payloads[i] = ""; + SequencerSignature memory sig = createSequencerSignature(messageIds[i], receiver, payloads[i], 0); vm.prank(sequencer); - bridge.initializeMessage(messageIds[i], receiver, payload, sig, 0); + bridge.initializeMessage(messageIds[i], receiver, payloads[i], sig, 0, 0); submitValidatorSignatures(messageIds[i]); } - bridge.batchHandleMessage(messageIds); + bool[] memory successes = bridge.batchHandleMessage(messageIds, payloads); for (uint256 i = 0; i < batchSize; i++) { assertTrue(bridge.isMessageCompleted(messageIds[i])); + assertTrue(successes[i]); } } + function test_BatchHandleMessage_PartialFailure() public { + bytes32[] memory messageIds = new bytes32[](3); + bytes[] memory payloads = new bytes[](3); + + // First message - will succeed + messageIds[0] = keccak256("batch-partial-0"); + payloads[0] = ""; + SequencerSignature memory sig0 = createSequencerSignature(messageIds[0], receiver, payloads[0], 0); + vm.prank(sequencer); + bridge.initializeMessage(messageIds[0], receiver, payloads[0], sig0, 0, 0); + submitValidatorSignatures(messageIds[0]); + + // Second message - will fail (not initialized) + messageIds[1] = keccak256("batch-partial-1-not-initialized"); + payloads[1] = ""; + + // Third message - will succeed + messageIds[2] = keccak256("batch-partial-2"); + payloads[2] = ""; + SequencerSignature memory sig2 = createSequencerSignature(messageIds[2], receiver, payloads[2], 0); + vm.prank(sequencer); + bridge.initializeMessage(messageIds[2], receiver, payloads[2], sig2, 0, 0); + submitValidatorSignatures(messageIds[2]); + + // Execute batch - should not revert, but second message should fail + bool[] memory successes = bridge.batchHandleMessage(messageIds, payloads); + + assertTrue(successes[0], "First message should succeed"); + assertFalse(successes[1], "Second message should fail"); + assertTrue(successes[2], "Third message should succeed"); + + assertTrue(bridge.isMessageCompleted(messageIds[0])); + assertFalse(bridge.isMessageInitialized(messageIds[1])); + assertTrue(bridge.isMessageCompleted(messageIds[2])); + } + /*////////////////////////////////////////////////////////////// FUZZING TESTS //////////////////////////////////////////////////////////////*/ @@ -680,14 +774,14 @@ contract BridgeTest is Test { assertTrue(depositSuccess); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, ethAmount); + bridge.initializeMessage(messageId, receiver, payload, sig, ethAmount, 0); submitValidatorSignatures(messageId); uint256 receiverBalanceBefore = receiver.balance; uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(receiver.balance, receiverBalanceBefore + ethAmount); assertEq(weth.balanceOf(address(bridge)), bridgeWethBefore - ethAmount); @@ -712,7 +806,7 @@ contract BridgeTest is Test { assertTrue(success); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, requiredAmount); + bridge.initializeMessage(messageId, receiver, payload, sig, requiredAmount, 0); vm.prank(address(bridge)); weth.transfer(user, stolenAmount); @@ -724,7 +818,7 @@ contract BridgeTest is Test { Bridge.InsufficientWrappedNativeTokenBalance.selector, requiredAmount, remainingAmount ) ); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function testFuzz_BatchInitialize_VariousAmounts(uint8 arrayLength, uint256 seed) public { @@ -738,13 +832,16 @@ contract BridgeTest is Test { uint256 totalEth = 0; + uint256 baseNonce = bridge.sequencerNonces(sequencer); for (uint256 i = 0; i < arrayLength; i++) { messageIds[i] = keccak256(abi.encodePacked("fuzz-batch", seed, i)); targetAddresses[i] = receiver; payloads[i] = ""; ethAmounts[i] = bound(uint256(keccak256(abi.encodePacked(seed, i))), 0, 10 ether); totalEth += ethAmounts[i]; - sigs[i] = createSequencerSignature(messageIds[i], targetAddresses[i], payloads[i], ethAmounts[i]); + sigs[i] = createSequencerSignatureWithNonce( + messageIds[i], targetAddresses[i], payloads[i], ethAmounts[i], 0, baseNonce + i + ); } vm.assume(totalEth <= type(uint96).max); @@ -758,8 +855,10 @@ contract BridgeTest is Test { uint256 bridgeWethBefore = weth.balanceOf(address(bridge)); + uint256[] memory deadlines = new uint256[](arrayLength); + vm.prank(sequencer); - bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts); + bridge.batchInitializeMessage(messageIds, targetAddresses, payloads, sigs, ethAmounts, deadlines); for (uint256 i = 0; i < arrayLength; i++) { assertTrue(bridge.isMessageInitialized(messageIds[i])); @@ -784,7 +883,7 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, ethAmount); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, ethAmount); + bridge.initializeMessage(messageId, receiver, payload, sig, ethAmount, 0); uint256 bridgeWethAfterInit = weth.balanceOf(address(bridge)); // WETH balance should remain the same (no wrapping during initializeMessage) @@ -794,7 +893,7 @@ contract BridgeTest is Test { submitValidatorSignatures(messageId); uint256 receiverBalanceBefore = receiver.balance; - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(receiver.balance, receiverBalanceBefore + ethAmount); assertEq(weth.balanceOf(address(bridge)), bridgeWethAfterInit - ethAmount); @@ -822,12 +921,12 @@ contract BridgeTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, address(returner), payload, sentAmount); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(returner), payload, sig, sentAmount); + bridge.initializeMessage(messageId, address(returner), payload, sig, sentAmount, 0); submitValidatorSignatures(messageId); // Handle message - bridge sends sentAmount ETH, returner returns returnedAmount ETH - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); // Bridge should have: original WETH - sentAmount + returnedAmount (re-wrapped) uint256 expectedWeth = bridgeWethBefore - sentAmount + returnedAmount; diff --git a/contracts/test/ModuleAddingAndRemovingTest.t.sol b/contracts/test/ModuleAddingAndRemovingTest.t.sol index 80928cbe..e82dab81 100644 --- a/contracts/test/ModuleAddingAndRemovingTest.t.sol +++ b/contracts/test/ModuleAddingAndRemovingTest.t.sol @@ -74,8 +74,11 @@ contract ModuleAddingAndRemovingTest is Test { bytes memory payload, uint256 nativeTokenAmount ) internal view returns (SequencerSignature memory) { + uint256 nonce = bridge.sequencerNonces(sequencer); bytes32 messageHash = keccak256( - abi.encodePacked(messageId, targetAddress, keccak256(payload), nativeTokenAmount) + abi.encodePacked( + block.chainid, messageId, targetAddress, keccak256(payload), nativeTokenAmount, uint256(0), nonce + ) ); bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerPrivateKey, ethSignedHash); @@ -212,12 +215,12 @@ contract ModuleAddingAndRemovingTest is Test { // Initialize message vm.startPrank(sequencer); - testBridge.initializeMessage(messageId, address(this), payload, sig, 0); + testBridge.initializeMessage(messageId, address(this), payload, sig, 0, 0); vm.stopPrank(); // Measure gas for handling message (which validates all modules) uint256 gasBeforeHandle = gasleft(); - try testBridge.handleMessage(messageId) { + try testBridge.handleMessage(messageId, payload) { uint256 gasUsed = gasBeforeHandle - gasleft(); emit log_named_uint("Module count", moduleCounts[j]); @@ -327,10 +330,10 @@ contract ModuleAddingAndRemovingTest is Test { SequencerSignature memory sig = createSequencerSignature(messageId, address(this), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(this), payload, sig, 0); + bridge.initializeMessage(messageId, address(this), payload, sig, 0, 0); uint256 gasBeforeHandle = gasleft(); - try bridge.handleMessage(messageId) { + try bridge.handleMessage(messageId, payload) { uint256 handleGas = gasBeforeHandle - gasleft(); emit log_named_uint("Handle message gas (20 modules)", handleGas); emit log_named_decimal_uint("% of block gas limit", (handleGas * 100) / 30_000_000, 2); diff --git a/contracts/test/use-cases/CrossChainMessagingTest.t.sol b/contracts/test/use-cases/CrossChainMessagingTest.t.sol index 2f39ac27..593f4848 100644 --- a/contracts/test/use-cases/CrossChainMessagingTest.t.sol +++ b/contracts/test/use-cases/CrossChainMessagingTest.t.sol @@ -58,12 +58,12 @@ contract CrossChainMessagingTest is UseCaseBaseTest { ); SequencerSignature memory sig = - createSequencerSignature(crossChainMessageId, address(destinationChain), payload, 0); + createSequencerSignature(bridge, crossChainMessageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(crossChainMessageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(crossChainMessageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, crossChainMessageId); - bridge.handleMessage(crossChainMessageId); + bridge.handleMessage(crossChainMessageId, payload); assertEq(token.balanceOf(recipient), mintAmount); } @@ -83,12 +83,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { destinationChain.receiveMintMessage.selector, messageId, address(token), recipient, amount, structuredData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(recipient), amount); } @@ -119,12 +120,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { crossChainData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(address(bridge)), bridgeAmount); assertEq(token.balanceOf(recipient), bridgeAmount); @@ -157,12 +159,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { crossChainData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } for (uint256 i = 0; i < messageCount; i++) { @@ -194,12 +197,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { destinationChain.receiveMintMessage.selector, messageId, address(token), recipient, amount, metadata ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(recipient), amount); } @@ -218,12 +222,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { destinationChain.receiveMintMessage.selector, messageId, address(token), recipient, amount, crossChainData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(recipient), amount); } @@ -241,12 +246,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { destinationChain.receiveMintMessage.selector, messageId, address(token), recipient, amount, emptyData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(recipient), amount); } @@ -264,12 +270,13 @@ contract CrossChainMessagingTest is UseCaseBaseTest { destinationChain.receiveMintMessage.selector, messageId, address(token), recipient, amount, largeData ); - SequencerSignature memory sig = createSequencerSignature(messageId, address(destinationChain), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(destinationChain), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0); + bridge.initializeMessage(messageId, address(destinationChain), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(token.balanceOf(recipient), amount); } @@ -296,12 +303,12 @@ contract CrossChainMessagingTest is UseCaseBaseTest { mintData ); SequencerSignature memory mintSig = - createSequencerSignature(mintMessageId, address(destinationChain), mintPayload, 0); + createSequencerSignature(bridge, mintMessageId, address(destinationChain), mintPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(mintMessageId, address(destinationChain), mintPayload, mintSig, 0); + bridge.initializeMessage(mintMessageId, address(destinationChain), mintPayload, mintSig, 0, 0); submitValidatorSignatures(bridge, mintMessageId); - bridge.handleMessage(mintMessageId); + bridge.handleMessage(mintMessageId, mintPayload); assertEq(token.balanceOf(recipient), totalAmount); @@ -320,12 +327,12 @@ contract CrossChainMessagingTest is UseCaseBaseTest { returnData ); SequencerSignature memory returnSig = - createSequencerSignature(returnMessageId, address(destinationChain), returnPayload, 0); + createSequencerSignature(bridge, returnMessageId, address(destinationChain), returnPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(returnMessageId, address(destinationChain), returnPayload, returnSig, 0); + bridge.initializeMessage(returnMessageId, address(destinationChain), returnPayload, returnSig, 0, 0); submitValidatorSignatures(bridge, returnMessageId); - bridge.handleMessage(returnMessageId); + bridge.handleMessage(returnMessageId, returnPayload); assertEq(token.balanceOf(recipient), totalAmount); assertEq(token.balanceOf(returnRecipient), returnAmount); diff --git a/contracts/test/use-cases/CrossChainNFTTest.t.sol b/contracts/test/use-cases/CrossChainNFTTest.t.sol index 09c4cfbd..be5b140c 100644 --- a/contracts/test/use-cases/CrossChainNFTTest.t.sol +++ b/contracts/test/use-cases/CrossChainNFTTest.t.sol @@ -72,7 +72,10 @@ contract CrossChainNFTTest is UseCaseBaseTest { view returns (SequencerSignature memory) { - bytes32 messageHash = keccak256(abi.encodePacked(messageId, targetAddress, keccak256(payload), uint256(0))); + uint256 nonce = destBridge.sequencerNonces(vm.addr(destSequencerPrivateKey)); + bytes32 messageHash = keccak256( + abi.encodePacked(block.chainid, messageId, targetAddress, keccak256(payload), uint256(0), uint256(0), nonce) + ); bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(destSequencerPrivateKey, ethSignedHash); return SequencerSignature({signature: abi.encodePacked(r, s, v), submittedAt: block.timestamp}); @@ -92,13 +95,14 @@ contract CrossChainNFTTest is UseCaseBaseTest { bytes memory burnPayload = abi.encodeWithSelector(onft.crosschainBurn.selector, user, tokenId); // Sequencer signature for burn - SequencerSignature memory burnSig = createSequencerSignature(burnMessageId, address(onft), burnPayload, 0); + SequencerSignature memory burnSig = + createSequencerSignature(sourceBridge, burnMessageId, address(onft), burnPayload, 0); // Step 3: Execute burn on source chain vm.prank(sequencer); - sourceBridge.initializeMessage(burnMessageId, address(onft), burnPayload, burnSig, 0); + sourceBridge.initializeMessage(burnMessageId, address(onft), burnPayload, burnSig, 0, 0); submitValidatorSignatures(sourceBridge, burnMessageId); - sourceBridge.handleMessage(burnMessageId); + sourceBridge.handleMessage(burnMessageId, burnPayload); // Verify NFT was burned vm.expectRevert(); @@ -118,8 +122,8 @@ contract CrossChainNFTTest is UseCaseBaseTest { // Execute mint on destination chain (no validators needed for this simple test) vm.prank(vm.addr(destSequencerPrivateKey)); - destBridge.initializeMessage(mintMessageId, address(receiver), mintPayload, mintSig, 0); - destBridge.handleMessage(mintMessageId); + destBridge.initializeMessage(mintMessageId, address(receiver), mintPayload, mintSig, 0, 0); + destBridge.handleMessage(mintMessageId, mintPayload); // Verify NFT was minted on destination chain assertEq(destOnft.ownerOf(tokenId), destinationUser); @@ -134,15 +138,15 @@ contract CrossChainNFTTest is UseCaseBaseTest { bytes32 messageId = keccak256("malicious-burn"); bytes memory payload = abi.encodeWithSelector(onft.crosschainBurn.selector, attacker, tokenId); - SequencerSignature memory sig = createSequencerSignature(messageId, address(onft), payload, 0); + SequencerSignature memory sig = createSequencerSignature(sourceBridge, messageId, address(onft), payload, 0); vm.prank(sequencer); - sourceBridge.initializeMessage(messageId, address(onft), payload, sig, 0); + sourceBridge.initializeMessage(messageId, address(onft), payload, sig, 0, 0); submitValidatorSignatures(sourceBridge, messageId); // Should revert because attacker doesn't own the token vm.expectRevert(); - sourceBridge.handleMessage(messageId); + sourceBridge.handleMessage(messageId, payload); } /// @notice Test only authorized bridge can mint ONFT @@ -181,12 +185,13 @@ contract CrossChainNFTTest is UseCaseBaseTest { bytes32 burnMsg1 = keccak256("burn-1"); bytes memory burnPayload1 = abi.encodeWithSelector(sourceOnft.crosschainBurn.selector, user, tokenId); - SequencerSignature memory sig1 = createSequencerSignature(burnMsg1, address(sourceOnft), burnPayload1, 0); + SequencerSignature memory sig1 = + createSequencerSignature(sourceBridge, burnMsg1, address(sourceOnft), burnPayload1, 0); vm.prank(sequencer); - sourceBridge.initializeMessage(burnMsg1, address(sourceOnft), burnPayload1, sig1, 0); + sourceBridge.initializeMessage(burnMsg1, address(sourceOnft), burnPayload1, sig1, 0, 0); submitValidatorSignatures(sourceBridge, burnMsg1); - sourceBridge.handleMessage(burnMsg1); + sourceBridge.handleMessage(burnMsg1, burnPayload1); // Verify burned on source vm.expectRevert(); @@ -198,8 +203,8 @@ contract CrossChainNFTTest is UseCaseBaseTest { SequencerSignature memory mintSig = createDestSequencerSignature(mintMsg, address(destOnft), mintPayload); vm.prank(vm.addr(destSequencerPrivateKey)); - destBridge.initializeMessage(mintMsg, address(destOnft), mintPayload, mintSig, 0); - destBridge.handleMessage(mintMsg); + destBridge.initializeMessage(mintMsg, address(destOnft), mintPayload, mintSig, 0, 0); + destBridge.handleMessage(mintMsg, mintPayload); assertEq(destOnft.ownerOf(tokenId), user); @@ -212,8 +217,8 @@ contract CrossChainNFTTest is UseCaseBaseTest { SequencerSignature memory burnSig2 = createDestSequencerSignature(burnMsg2, address(destOnft), burnPayload2); vm.prank(vm.addr(destSequencerPrivateKey)); - destBridge.initializeMessage(burnMsg2, address(destOnft), burnPayload2, burnSig2, 0); - destBridge.handleMessage(burnMsg2); + destBridge.initializeMessage(burnMsg2, address(destOnft), burnPayload2, burnSig2, 0, 0); + destBridge.handleMessage(burnMsg2, burnPayload2); // Verify burned on dest vm.expectRevert(); @@ -222,12 +227,13 @@ contract CrossChainNFTTest is UseCaseBaseTest { // Step 4: Re-mint on source bytes32 mintMsg2 = keccak256("mint-source"); bytes memory mintPayload2 = abi.encodeWithSelector(sourceOnft.crosschainMint.selector, user, tokenId); - SequencerSignature memory mintSig2 = createSequencerSignature(mintMsg2, address(sourceOnft), mintPayload2, 0); + SequencerSignature memory mintSig2 = + createSequencerSignature(sourceBridge, mintMsg2, address(sourceOnft), mintPayload2, 0); vm.prank(sequencer); - sourceBridge.initializeMessage(mintMsg2, address(sourceOnft), mintPayload2, mintSig2, 0); + sourceBridge.initializeMessage(mintMsg2, address(sourceOnft), mintPayload2, mintSig2, 0, 0); submitValidatorSignatures(sourceBridge, mintMsg2); - sourceBridge.handleMessage(mintMsg2); + sourceBridge.handleMessage(mintMsg2, mintPayload2); // Verify NFT is back on source chain with original owner assertEq(sourceOnft.ownerOf(tokenId), user); @@ -257,12 +263,13 @@ contract CrossChainNFTTest is UseCaseBaseTest { for (uint256 i = 0; i < 3; i++) { bytes32 messageId = keccak256(abi.encodePacked("burn-batch", i)); bytes memory payload = abi.encodeWithSelector(batchOnft.crosschainBurn.selector, user, i); - SequencerSignature memory sig = createSequencerSignature(messageId, address(batchOnft), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(sourceBridge, messageId, address(batchOnft), payload, 0); vm.prank(sequencer); - sourceBridge.initializeMessage(messageId, address(batchOnft), payload, sig, 0); + sourceBridge.initializeMessage(messageId, address(batchOnft), payload, sig, 0, 0); submitValidatorSignatures(sourceBridge, messageId); - sourceBridge.handleMessage(messageId); + sourceBridge.handleMessage(messageId, payload); } // Verify all burned @@ -289,12 +296,13 @@ contract CrossChainNFTTest is UseCaseBaseTest { bytes32 messageId = keccak256("burn-999"); bytes memory payload = abi.encodeWithSelector(multiOnft.crosschainBurn.selector, user, 999); - SequencerSignature memory sig = createSequencerSignature(messageId, address(multiOnft), payload, 0); + SequencerSignature memory sig = + createSequencerSignature(sourceBridge, messageId, address(multiOnft), payload, 0); vm.prank(sequencer); - sourceBridge.initializeMessage(messageId, address(multiOnft), payload, sig, 0); + sourceBridge.initializeMessage(messageId, address(multiOnft), payload, sig, 0, 0); submitValidatorSignatures(sourceBridge, messageId); - sourceBridge.handleMessage(messageId); + sourceBridge.handleMessage(messageId, payload); // Verify only token 999 was burned assertEq(multiOnft.ownerOf(42), user); diff --git a/contracts/test/use-cases/ERC20TransferTest.t.sol b/contracts/test/use-cases/ERC20TransferTest.t.sol index 321f0d18..6b0b42d9 100644 --- a/contracts/test/use-cases/ERC20TransferTest.t.sol +++ b/contracts/test/use-cases/ERC20TransferTest.t.sol @@ -55,14 +55,14 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("erc20-transfer-1"); bytes memory payload = abi.encodeWithSelector(usdc.transfer.selector, recipient, transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(usdc.balanceOf(recipient), transferAmount); assertEq(usdc.balanceOf(address(bridge)), 0); @@ -77,14 +77,14 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("erc20-transferFrom-1"); bytes memory payload = abi.encodeWithSelector(usdc.transferFrom.selector, user, recipient, transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(usdc.balanceOf(recipient), transferAmount); assertEq(usdc.balanceOf(user), INITIAL_BALANCE - transferAmount); @@ -109,32 +109,34 @@ contract ERC20TransferTest is UseCaseBaseTest { // USDC transfer bytes32 usdcMessageId = keccak256("multi-usdc"); bytes memory usdcPayload = abi.encodeWithSelector(usdc.transfer.selector, recipient, usdcAmount); - SequencerSignature memory usdcSig = createSequencerSignature(usdcMessageId, address(usdc), usdcPayload, 0); + SequencerSignature memory usdcSig = + createSequencerSignature(bridge, usdcMessageId, address(usdc), usdcPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(usdcMessageId, address(usdc), usdcPayload, usdcSig, 0); + bridge.initializeMessage(usdcMessageId, address(usdc), usdcPayload, usdcSig, 0, 0); submitValidatorSignatures(bridge, usdcMessageId); - bridge.handleMessage(usdcMessageId); + bridge.handleMessage(usdcMessageId, usdcPayload); // DAI transfer bytes32 daiMessageId = keccak256("multi-dai"); bytes memory daiPayload = abi.encodeWithSelector(dai.transfer.selector, recipient, daiAmount); - SequencerSignature memory daiSig = createSequencerSignature(daiMessageId, address(dai), daiPayload, 0); + SequencerSignature memory daiSig = createSequencerSignature(bridge, daiMessageId, address(dai), daiPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(daiMessageId, address(dai), daiPayload, daiSig, 0); + bridge.initializeMessage(daiMessageId, address(dai), daiPayload, daiSig, 0, 0); submitValidatorSignatures(bridge, daiMessageId); - bridge.handleMessage(daiMessageId); + bridge.handleMessage(daiMessageId, daiPayload); // WETH transfer bytes32 wethMessageId = keccak256("multi-weth"); bytes memory wethPayload = abi.encodeWithSelector(weth.transfer.selector, recipient, wethAmount); - SequencerSignature memory wethSig = createSequencerSignature(wethMessageId, address(weth), wethPayload, 0); + SequencerSignature memory wethSig = + createSequencerSignature(bridge, wethMessageId, address(weth), wethPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(wethMessageId, address(weth), wethPayload, wethSig, 0); + bridge.initializeMessage(wethMessageId, address(weth), wethPayload, wethSig, 0, 0); submitValidatorSignatures(bridge, wethMessageId); - bridge.handleMessage(wethMessageId); + bridge.handleMessage(wethMessageId, wethPayload); assertEq(usdc.balanceOf(recipient), usdcAmount); assertEq(dai.balanceOf(recipient), daiAmount); @@ -160,12 +162,12 @@ contract ERC20TransferTest is UseCaseBaseTest { for (uint256 i = 0; i < recipients.length; i++) { bytes32 messageId = keccak256(abi.encodePacked("batch-transfer", i)); bytes memory payload = abi.encodeWithSelector(usdc.transfer.selector, recipients[i], amountEach); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } for (uint256 i = 0; i < recipients.length; i++) { @@ -188,12 +190,13 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 approveMessageId = keccak256("approve-test"); bytes memory approvePayload = abi.encodeWithSelector(usdc.approve.selector, recipient, approvalAmount); - SequencerSignature memory sig = createSequencerSignature(approveMessageId, address(usdc), approvePayload, 0); + SequencerSignature memory sig = + createSequencerSignature(bridge, approveMessageId, address(usdc), approvePayload, 0); vm.prank(sequencer); - bridge.initializeMessage(approveMessageId, address(usdc), approvePayload, sig, 0); + bridge.initializeMessage(approveMessageId, address(usdc), approvePayload, sig, 0, 0); submitValidatorSignatures(bridge, approveMessageId); - bridge.handleMessage(approveMessageId); + bridge.handleMessage(approveMessageId, approvePayload); assertEq(usdc.allowance(address(bridge), recipient), approvalAmount); @@ -217,14 +220,14 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("insufficient-balance"); bytes memory payload = abi.encodeWithSelector(usdc.transfer.selector, recipient, transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); vm.expectRevert(); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_TransferToZeroAddress_Reverts() public { @@ -236,14 +239,14 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("zero-address"); bytes memory payload = abi.encodeWithSelector(usdc.transfer.selector, address(0), amount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); vm.expectRevert(); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_TransferZeroAmount() public { @@ -253,12 +256,12 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("zero-amount"); bytes memory payload = abi.encodeWithSelector(usdc.transfer.selector, recipient, 0); - SequencerSignature memory sig = createSequencerSignature(messageId, address(usdc), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(usdc), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(usdc), payload, sig, 0); + bridge.initializeMessage(messageId, address(usdc), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(usdc.balanceOf(recipient), 0); } @@ -281,24 +284,25 @@ contract ERC20TransferTest is UseCaseBaseTest { bytes32 transferMessageId = keccak256("complete-transfer"); bytes memory transferPayload = abi.encodeWithSelector(usdc.transfer.selector, recipient, transferAmount); SequencerSignature memory transferSig = - createSequencerSignature(transferMessageId, address(usdc), transferPayload, 0); + createSequencerSignature(bridge, transferMessageId, address(usdc), transferPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(transferMessageId, address(usdc), transferPayload, transferSig, 0); + bridge.initializeMessage(transferMessageId, address(usdc), transferPayload, transferSig, 0, 0); submitValidatorSignatures(bridge, transferMessageId); - bridge.handleMessage(transferMessageId); + bridge.handleMessage(transferMessageId, transferPayload); assertEq(usdc.balanceOf(recipient), transferAmount); // Return remaining to user bytes32 returnMessageId = keccak256("complete-return"); bytes memory returnPayload = abi.encodeWithSelector(usdc.transfer.selector, user, remainingAmount); - SequencerSignature memory returnSig = createSequencerSignature(returnMessageId, address(usdc), returnPayload, 0); + SequencerSignature memory returnSig = + createSequencerSignature(bridge, returnMessageId, address(usdc), returnPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(returnMessageId, address(usdc), returnPayload, returnSig, 0); + bridge.initializeMessage(returnMessageId, address(usdc), returnPayload, returnSig, 0, 0); submitValidatorSignatures(bridge, returnMessageId); - bridge.handleMessage(returnMessageId); + bridge.handleMessage(returnMessageId, returnPayload); assertEq(usdc.balanceOf(user), INITIAL_BALANCE - transferAmount); assertEq(usdc.balanceOf(recipient), transferAmount); diff --git a/contracts/test/use-cases/ETHTransferTest.t.sol b/contracts/test/use-cases/ETHTransferTest.t.sol index faeaae4e..fa15dcf7 100644 --- a/contracts/test/use-cases/ETHTransferTest.t.sol +++ b/contracts/test/use-cases/ETHTransferTest.t.sol @@ -59,15 +59,15 @@ contract ETHTransferTest is UseCaseBaseTest { bytes32 messageId = keccak256(abi.encodePacked("transfer", block.timestamp)); bytes memory payload = abi.encodeWithSelector(weth.transfer.selector, address(recipient), transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(weth), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(weth), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(weth), payload, sig, 0); + bridge.initializeMessage(messageId, address(weth), payload, sig, 0, 0); // Submit validator signatures (2 out of 3 threshold) submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(weth.balanceOf(address(recipient)), transferAmount); assertEq(weth.balanceOf(address(bridge)), 0); @@ -108,15 +108,15 @@ contract ETHTransferTest is UseCaseBaseTest { bytes32 messageId = keccak256(abi.encodePacked("transfer", i)); bytes memory payload = abi.encodeWithSelector(weth.transfer.selector, address(recipient), amounts[i]); - SequencerSignature memory sig = createSequencerSignature(messageId, address(weth), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(weth), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(weth), payload, sig, 0); + bridge.initializeMessage(messageId, address(weth), payload, sig, 0, 0); // Submit validator signatures submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } uint256 expectedTotal = amounts[0] + amounts[1] + amounts[2]; @@ -138,17 +138,17 @@ contract ETHTransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("insufficient-sigs"); bytes memory payload = abi.encodeWithSelector(weth.transfer.selector, address(recipient), transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(weth), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(weth), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(weth), payload, sig, 0); + bridge.initializeMessage(messageId, address(weth), payload, sig, 0, 0); // Only submit 1 signature (threshold is 2) submitValidatorSignatures(bridge, messageId, 1); // Should revert due to insufficient signatures vm.expectRevert(); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } /// @notice Test that message succeeds with exact threshold @@ -162,15 +162,15 @@ contract ETHTransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("exact-threshold"); bytes memory payload = abi.encodeWithSelector(weth.transfer.selector, address(recipient), transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(weth), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(weth), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(weth), payload, sig, 0); + bridge.initializeMessage(messageId, address(weth), payload, sig, 0, 0); // Submit exactly 2 signatures (threshold is 2) submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(weth.balanceOf(address(recipient)), transferAmount); } @@ -186,15 +186,15 @@ contract ETHTransferTest is UseCaseBaseTest { bytes32 messageId = keccak256("more-than-threshold"); bytes memory payload = abi.encodeWithSelector(weth.transfer.selector, address(recipient), transferAmount); - SequencerSignature memory sig = createSequencerSignature(messageId, address(weth), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(weth), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(weth), payload, sig, 0); + bridge.initializeMessage(messageId, address(weth), payload, sig, 0, 0); // Submit all 3 signatures (threshold is 2) submitValidatorSignatures(bridge, messageId, 3); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(weth.balanceOf(address(recipient)), transferAmount); } diff --git a/contracts/test/use-cases/MessageOrderingTest.t.sol b/contracts/test/use-cases/MessageOrderingTest.t.sol index 6b1d8ac3..922663f4 100644 --- a/contracts/test/use-cases/MessageOrderingTest.t.sol +++ b/contracts/test/use-cases/MessageOrderingTest.t.sol @@ -63,20 +63,20 @@ contract MessageOrderingTest is UseCaseBaseTest { for (uint256 nonce = 0; nonce < 3; nonce++) { bytes32 messageId = keccak256(abi.encodePacked("msg", nonce)); bytes memory payload = ""; - SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, receiver, payload, 0); // Validate nonce first orderingModule.validateNonce(identifier, nonce); // Initialize message vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); // Submit validator signatures _submitValidatorSignatures(messageId); // Handle message - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertTrue(bridge.isMessageCompleted(messageId)); } @@ -106,12 +106,12 @@ contract MessageOrderingTest is UseCaseBaseTest { bytes32 messageId = keccak256(abi.encodePacked("user1-msg", nonce)); bytes memory payload = ""; - SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, receiver, payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); _submitValidatorSignatures(messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } // User2 sends 3 messages (independent sequence) @@ -120,12 +120,12 @@ contract MessageOrderingTest is UseCaseBaseTest { bytes32 messageId = keccak256(abi.encodePacked("user2-msg", nonce)); bytes memory payload = ""; - SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, receiver, payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); _submitValidatorSignatures(messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } // Verify independent nonce tracking @@ -222,13 +222,13 @@ contract MessageOrderingTest is UseCaseBaseTest { // Create message bytes32 messageId = keccak256(abi.encodePacked("swap", nonce)); bytes memory payload = abi.encodeWithSignature(swaps[nonce]); - SequencerSignature memory sig = createSequencerSignature(messageId, receiver, payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, receiver, payload, 0); // Initialize and execute vm.prank(sequencer); - bridge.initializeMessage(messageId, receiver, payload, sig, 0); + bridge.initializeMessage(messageId, receiver, payload, sig, 0, 0); _submitValidatorSignatures(messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertTrue(bridge.isMessageCompleted(messageId)); } diff --git a/contracts/test/use-cases/NFTMintingTest.t.sol b/contracts/test/use-cases/NFTMintingTest.t.sol index 69c95234..1a97a04b 100644 --- a/contracts/test/use-cases/NFTMintingTest.t.sol +++ b/contracts/test/use-cases/NFTMintingTest.t.sol @@ -45,14 +45,14 @@ contract NFTMintingTest is UseCaseBaseTest { function test_MintFreeNFT() public { bytes32 messageId = keccak256("free-mint-1"); bytes memory payload = abi.encodeWithSelector(freeNFT.mint.selector, user); - SequencerSignature memory sig = createSequencerSignature(messageId, address(freeNFT), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(freeNFT), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0); + bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(freeNFT.ownerOf(0), user); assertEq(freeNFT.balanceOf(user), 1); @@ -64,14 +64,14 @@ contract NFTMintingTest is UseCaseBaseTest { for (uint256 i = 0; i < mintCount; i++) { bytes32 messageId = keccak256(abi.encodePacked("free-mint", i)); bytes memory payload = abi.encodeWithSelector(freeNFT.mint.selector, user); - SequencerSignature memory sig = createSequencerSignature(messageId, address(freeNFT), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(freeNFT), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0); + bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } assertEq(freeNFT.balanceOf(user), mintCount); @@ -90,14 +90,14 @@ contract NFTMintingTest is UseCaseBaseTest { for (uint256 i = 0; i < recipients.length; i++) { bytes32 messageId = keccak256(abi.encodePacked("batch-mint", i)); bytes memory payload = abi.encodeWithSelector(freeNFT.mint.selector, recipients[i]); - SequencerSignature memory sig = createSequencerSignature(messageId, address(freeNFT), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(freeNFT), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0); + bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } for (uint256 i = 0; i < recipients.length; i++) { @@ -119,25 +119,26 @@ contract NFTMintingTest is UseCaseBaseTest { bytes32 approveMessageId = keccak256("approve-weth"); bytes memory approvePayload = abi.encodeWithSelector(weth.approve.selector, address(paidNFT), NFT_PRICE); SequencerSignature memory approveSig = - createSequencerSignature(approveMessageId, address(weth), approvePayload, 0); + createSequencerSignature(bridge, approveMessageId, address(weth), approvePayload, 0); vm.prank(sequencer); - bridge.initializeMessage(approveMessageId, address(weth), approvePayload, approveSig, 0); + bridge.initializeMessage(approveMessageId, address(weth), approvePayload, approveSig, 0, 0); submitValidatorSignatures(bridge, approveMessageId); - bridge.handleMessage(approveMessageId); + bridge.handleMessage(approveMessageId, approvePayload); bytes32 mintMessageId = keccak256("paid-mint-1"); bytes memory mintPayload = abi.encodeWithSelector(paidNFT.mintWithWETH.selector, user, address(weth), NFT_PRICE); - SequencerSignature memory mintSig = createSequencerSignature(mintMessageId, address(paidNFT), mintPayload, 0); + SequencerSignature memory mintSig = + createSequencerSignature(bridge, mintMessageId, address(paidNFT), mintPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(mintMessageId, address(paidNFT), mintPayload, mintSig, 0); + bridge.initializeMessage(mintMessageId, address(paidNFT), mintPayload, mintSig, 0, 0); submitValidatorSignatures(bridge, mintMessageId); - bridge.handleMessage(mintMessageId); + bridge.handleMessage(mintMessageId, mintPayload); assertEq(paidNFT.ownerOf(0), user); assertEq(paidNFT.balanceOf(user), 1); @@ -156,27 +157,28 @@ contract NFTMintingTest is UseCaseBaseTest { bytes memory approvePayload = abi.encodeWithSelector(weth.approve.selector, address(paidNFT), insufficientAmount); SequencerSignature memory approveSig = - createSequencerSignature(approveMessageId, address(weth), approvePayload, 0); + createSequencerSignature(bridge, approveMessageId, address(weth), approvePayload, 0); vm.prank(sequencer); - bridge.initializeMessage(approveMessageId, address(weth), approvePayload, approveSig, 0); + bridge.initializeMessage(approveMessageId, address(weth), approvePayload, approveSig, 0, 0); submitValidatorSignatures(bridge, approveMessageId); - bridge.handleMessage(approveMessageId); + bridge.handleMessage(approveMessageId, approvePayload); bytes32 mintMessageId = keccak256("paid-mint-insufficient"); bytes memory mintPayload = abi.encodeWithSelector(paidNFT.mintWithWETH.selector, user, address(weth), insufficientAmount); - SequencerSignature memory mintSig = createSequencerSignature(mintMessageId, address(paidNFT), mintPayload, 0); + SequencerSignature memory mintSig = + createSequencerSignature(bridge, mintMessageId, address(paidNFT), mintPayload, 0); vm.prank(sequencer); - bridge.initializeMessage(mintMessageId, address(paidNFT), mintPayload, mintSig, 0); + bridge.initializeMessage(mintMessageId, address(paidNFT), mintPayload, mintSig, 0, 0); submitValidatorSignatures(bridge, mintMessageId); vm.expectRevert(); - bridge.handleMessage(mintMessageId); + bridge.handleMessage(mintMessageId, mintPayload); } /// @notice Test minting NFT with native ETH @@ -191,13 +193,14 @@ contract NFTMintingTest is UseCaseBaseTest { // Bridge calls paidNFT to unwrap WETH and mint NFT with ETH bytes32 messageId = keccak256("withdraw-and-mint"); bytes memory payload = abi.encodeWithSelector(paidNFT.mintWithPayment.selector, user); - SequencerSignature memory sig = createSequencerSignature(messageId, address(paidNFT), payload, NFT_PRICE); + SequencerSignature memory sig = + createSequencerSignature(bridge, messageId, address(paidNFT), payload, NFT_PRICE); // Bridge calls paidNFT with ETH payment vm.prank(sequencer); - bridge.initializeMessage(messageId, address(paidNFT), payload, sig, NFT_PRICE); + bridge.initializeMessage(messageId, address(paidNFT), payload, sig, NFT_PRICE, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); // Verify NFT was minted to user assertEq(paidNFT.ownerOf(0), user); @@ -218,28 +221,28 @@ contract NFTMintingTest is UseCaseBaseTest { function test_MintToZeroAddress_Reverts() public { bytes32 messageId = keccak256("mint-zero"); bytes memory payload = abi.encodeWithSelector(freeNFT.mint.selector, address(0)); - SequencerSignature memory sig = createSequencerSignature(messageId, address(freeNFT), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(freeNFT), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0); + bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); vm.expectRevert(); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); } function test_BridgeIsMsgSender() public { bytes32 messageId = keccak256("sender-test"); bytes memory payload = abi.encodeWithSelector(freeNFT.mint.selector, user); - SequencerSignature memory sig = createSequencerSignature(messageId, address(freeNFT), payload, 0); + SequencerSignature memory sig = createSequencerSignature(bridge, messageId, address(freeNFT), payload, 0); vm.prank(sequencer); - bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0); + bridge.initializeMessage(messageId, address(freeNFT), payload, sig, 0, 0); submitValidatorSignatures(bridge, messageId); - bridge.handleMessage(messageId); + bridge.handleMessage(messageId, payload); assertEq(freeNFT.balanceOf(user), 1); } diff --git a/contracts/test/use-cases/base/UseCaseBaseTest.sol b/contracts/test/use-cases/base/UseCaseBaseTest.sol index 489022f9..c5a8183f 100644 --- a/contracts/test/use-cases/base/UseCaseBaseTest.sol +++ b/contracts/test/use-cases/base/UseCaseBaseTest.sol @@ -56,21 +56,38 @@ abstract contract UseCaseBaseTest is Test { bridge.setMessageInitializer(sequencer, true); } - /// @notice Create a sequencer signature for a message + /// @notice Create a sequencer signature for a message (includes chain ID, nonce, and deadline) function createSequencerSignature( + Bridge bridge, bytes32 messageId, address targetAddress, bytes memory payload, - uint256 nativeTokenAmount + uint256 nativeTokenAmount, + uint256 deadline ) internal view returns (SequencerSignature memory) { + address sequencerAddr = vm.addr(sequencerPrivateKey); + uint256 nonce = bridge.sequencerNonces(sequencerAddr); bytes32 messageHash = keccak256( - abi.encodePacked(messageId, targetAddress, keccak256(payload), nativeTokenAmount) + abi.encodePacked( + block.chainid, messageId, targetAddress, keccak256(payload), nativeTokenAmount, deadline, nonce + ) ); bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerPrivateKey, ethSignedHash); return SequencerSignature({signature: abi.encodePacked(r, s, v), submittedAt: block.timestamp}); } + /// @notice Convenience overload without deadline (defaults to 0 = no deadline) + function createSequencerSignature( + Bridge bridge, + bytes32 messageId, + address targetAddress, + bytes memory payload, + uint256 nativeTokenAmount + ) internal view returns (SequencerSignature memory) { + return createSequencerSignature(bridge, messageId, targetAddress, payload, nativeTokenAmount, 0); + } + /// @notice Setup bridge with validators and validator module /// @param bridge The bridge contract /// @param threshold The signature threshold (defaults to 2)