diff --git a/contracts/protocol/SocketBatcher.sol b/contracts/protocol/SocketBatcher.sol index 25d24711..61010e33 100644 --- a/contracts/protocol/SocketBatcher.sol +++ b/contracts/protocol/SocketBatcher.sol @@ -56,7 +56,11 @@ contract SocketBatcher is ISocketBatcher, Ownable { bytes calldata proof_ ) external payable returns (bool, bytes memory) { // Attest digest on FastSwitchboard - IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(executionParams_.payloadId, digest_, proof_); + IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest( + executionParams_.payloadId, + digest_, + proof_ + ); // Execute payload on socket return socket__.execute{value: msg.value}(executionParams_, transmissionParams_); } diff --git a/contracts/protocol/switchboard/EVMxSwitchboard.sol b/contracts/protocol/switchboard/EVMxSwitchboard.sol index d422f12b..ca95a1e3 100644 --- a/contracts/protocol/switchboard/EVMxSwitchboard.sol +++ b/contracts/protocol/switchboard/EVMxSwitchboard.sol @@ -68,6 +68,14 @@ contract EVMxSwitchboard is SwitchboardBase { bytes payload ); + struct AssignTransmitterParams { + DigestParams digestParams; + address oldTransmitter; + address newTransmitter; + uint256 nonce; + bytes[] signatures; // must be totalWatchers length + } + // --- Constructor --- constructor( @@ -86,7 +94,6 @@ contract EVMxSwitchboard is SwitchboardBase { } // --- External Functions --- - /** * @notice Attests a payload digest with watcher signature * @param digest_ The digest of the payload to be executed @@ -95,23 +102,52 @@ contract EVMxSwitchboard is SwitchboardBase { * Payload is uniquely identified by digest. Once attested, payload can be executed. */ function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) public virtual { - address watcher = _recoverSigner( - keccak256( - abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) - ), - proof_ + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) + ); + address watcher = _validateSignature(messageHash, proof_, WATCHER_ROLE); + + _processAttestation(payloadId_, digest_, watcher); + } + + /** + * @notice Batch attests a payload digest with multiple watcher signatures + * @param payloadId_ The payload ID to attest + * @param digest_ The digest of the payload to be executed + * @param proofs_ Array of watcher signature proofs + * @dev Processes multiple attestations in a single transaction to save gas. + * Reverts if any watcher is not authorized or has already attested. + * Can be called by any third party as authorization happens through signatures. + */ + function batchAttest( + bytes32 payloadId_, + bytes32 digest_, + bytes[] memory proofs_ + ) public virtual { + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) ); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + address[] memory watchers = _validateBatchSignatures(messageHash, proofs_, WATCHER_ROLE); - // Prevent double attestation - if (isAttestedByWatcher[watcher][payloadId_][digest_]) revert AlreadyAttested(); - isAttestedByWatcher[watcher][payloadId_][digest_] = true; + for (uint256 i = 0; i < watchers.length; i++) { + _processAttestation(payloadId_, digest_, watchers[i]); + } + } + + /** + * @notice Processes a single watcher attestation + * @param payloadId_ The payload ID + * @param digest_ The digest of the payload + * @param watcher_ The watcher address + * @dev Checks for double attestation, updates state, and emits event + */ + function _processAttestation(bytes32 payloadId_, bytes32 digest_, address watcher_) internal { + if (isAttestedByWatcher[watcher_][payloadId_][digest_]) revert AlreadyAttested(); + isAttestedByWatcher[watcher_][payloadId_][digest_] = true; attestations[payloadId_][digest_]++; - // Mark digest as valid if enough attestations are reached if (attestations[payloadId_][digest_] >= totalWatchers) isValid[payloadId_][digest_] = true; - - emit Attested(payloadId_, digest_, watcher); + emit Attested(payloadId_, digest_, watcher_); } /** @@ -212,17 +248,20 @@ contract EVMxSwitchboard is SwitchboardBase { } /** - * @notice Sets reverting status for a payload - * @param payloadId_ The payload ID to mark - * @param isReverting_ True if payload should be marked as reverting - * @dev Only callable by owner. Used to mark payloads that are known to revert. + * @notice Sets reverting payload status with watcher signatures + * @param payloadId_ payload ID to mark + * @param isReverting_ reverting status flag + * @param nonce_ nonce to prevent replay attacks + * @param signatures_ watcher signature + * @dev Processes multiple payloads in a single transaction. Each payload requires exactly totalWatchers signatures. */ function setRevertingPayload( bytes32 payloadId_, bool isReverting_, uint256 nonce_, - bytes calldata signature_ + bytes[] memory signatures_ ) external { + if (signatures_.length != totalWatchers) revert ArrayLengthMismatch(); bytes32 digest = keccak256( abi.encodePacked( toBytes32Format(address(this)), @@ -233,54 +272,35 @@ contract EVMxSwitchboard is SwitchboardBase { ) ); - address watcher = _recoverSigner(digest, signature_); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed(); - usedNonces[watcher][nonce_] = true; + for (uint256 k = 0; k < totalWatchers; k++) { + address watcher = _validateSignature(digest, signatures_[k], WATCHER_ROLE); + _validateAndUseNonce(this.setRevertingPayload.selector, watcher, nonce_); + } revertingPayloadIds[payloadId_] = isReverting_; - emit RevertingPayloadIdset(payloadId_, isReverting_); + emit RevertingPayloadIdSet(payloadId_, isReverting_); } - /** - * @notice Gets the transmitter address for payload execution - * @param digestParams_ The digest parameters - * @param signature_ The watcher signature - * @dev Only callable by watcher. Used to assign the transmitter address for payload execution. - */ - function assignTransmitter( - DigestParams memory digestParams_, - address oldTransmitter_, - address newTransmitter_, - uint256 nonce_, - bytes calldata signature_ - ) external { - bytes32 oldTransmitterBytes32 = toBytes32Format(oldTransmitter_); - digestParams_.transmitter = oldTransmitterBytes32; - bytes32 oldDigest = createDigest(digestParams_); - - if (payloadIdToDigest[digestParams_.payloadId] != oldDigest) revert InvalidDigest(); - - digestParams_.transmitter = toBytes32Format(newTransmitter_); - bytes32 newDigest = createDigest(digestParams_); - - address watcher = _recoverSigner( - keccak256( - abi.encodePacked( - toBytes32Format(address(this)), - chainSlug, - oldDigest, - newDigest, - nonce_ - ) - ), - signature_ + function assignTransmitter(AssignTransmitterParams memory params_) external { + if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch(); + + DigestParams memory dp = params_.digestParams; + dp.transmitter = toBytes32Format(params_.oldTransmitter); + bytes32 oldDigest = createDigest(dp); + if (payloadIdToDigest[dp.payloadId] != oldDigest) revert InvalidDigest(); + + dp.transmitter = toBytes32Format(params_.newTransmitter); + bytes32 newDigest = createDigest(dp); + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, oldDigest, newDigest, params_.nonce) ); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - _validateAndUseNonce(this.assignTransmitter.selector, watcher, nonce_); + for (uint256 k = 0; k < totalWatchers; k++) { + address watcher = _validateSignature(messageHash, params_.signatures[k], WATCHER_ROLE); + _validateAndUseNonce(this.assignTransmitter.selector, watcher, params_.nonce); + } - payloadIdToDigest[digestParams_.payloadId] = newDigest; - emit TransmitterAssigned(digestParams_.payloadId, newTransmitter_); + payloadIdToDigest[dp.payloadId] = newDigest; + emit TransmitterAssigned(dp.payloadId, params_.newTransmitter); } function markIsValid(bytes32 payloadId_, bytes32 digest_) external { diff --git a/contracts/protocol/switchboard/MessageSwitchboard.sol b/contracts/protocol/switchboard/MessageSwitchboard.sol index dbacb999..1dd8f1ae 100644 --- a/contracts/protocol/switchboard/MessageSwitchboard.sol +++ b/contracts/protocol/switchboard/MessageSwitchboard.sol @@ -76,11 +76,19 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { /// @notice Event emitted when sponsor revokes a plug event PlugRevoked(address indexed sponsor, address indexed plug); + struct AssignTransmitterParams { + DigestParams digestParams; + address oldTransmitter; + address newTransmitter; + uint256 nonce; + bytes[] signatures; // must be totalWatchers length + } + /// @notice Event emitted when plug configuration is updated event PlugConfigUpdated(address indexed plug, uint32 indexed chainSlug, bytes32 siblingPlug); /// @notice Event emitted when refund eligibility is marked by watcher - event RefundEligibilityMarked(bytes32 indexed payloadId, address indexed watcher); + event RefundEligibilityMarked(bytes32 indexed payloadId); /// @notice Event emitted when refund is issued event Refunded(bytes32 indexed payloadId, address indexed refundAddress, uint256 amount); @@ -133,26 +141,47 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { * @dev Enhanced attestation verifies target with sibling chain slug and sibling plug. */ function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) public { - // Recover watcher from signature - address watcher = _recoverSigner( - keccak256( - abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) - ), - proof_ + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) ); + address watcher = _validateSignature(messageHash, proof_, WATCHER_ROLE); - // Verify watcher has WATCHER_ROLE - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + _processAttestation(payloadId_, digest_, watcher); + } - // Prevent double attestation - if (isAttestedByWatcher[watcher][payloadId_][digest_]) revert AlreadyAttested(); - isAttestedByWatcher[watcher][payloadId_][digest_] = true; - attestations[payloadId_][digest_]++; + /** + * @notice Batch attests a payload with multiple watcher signatures + * @param payloadId_ The payload ID to attest + * @param digest_ The digest of the payload to be executed + * @param proofs_ Array of watcher signature proofs + * @dev Processes multiple attestations in a single transaction to save gas. + * Reverts if any watcher is not authorized or has already attested. + * Can be called by any third party as authorization happens through signatures. + */ + function batchAttest(bytes32 payloadId_, bytes32 digest_, bytes[] memory proofs_) public { + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_) + ); + address[] memory watchers = _validateBatchSignatures(messageHash, proofs_, WATCHER_ROLE); + for (uint256 i = 0; i < watchers.length; i++) { + _processAttestation(payloadId_, digest_, watchers[i]); + } + } - // Mark digest_ as valid if enough attestations are reached + /** + * @notice Processes a single watcher attestation + * @param payloadId_ The payload ID + * @param digest_ The digest of the payload + * @param watcher_ The watcher address + * @dev Checks for double attestation, updates state, and emits event + */ + function _processAttestation(bytes32 payloadId_, bytes32 digest_, address watcher_) internal { + if (isAttestedByWatcher[watcher_][payloadId_][digest_]) revert AlreadyAttested(); + isAttestedByWatcher[watcher_][payloadId_][digest_] = true; + attestations[payloadId_][digest_]++; if (attestations[payloadId_][digest_] >= totalWatchers) isValid[payloadId_][digest_] = true; - emit Attested(payloadId_, digest_, watcher); + emit Attested(payloadId_, digest_, watcher_); } /** @@ -444,28 +473,35 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { } /** - * @dev Mark a payload as eligible for refund (called with watcher signature) + * @dev Mark a payload as eligible for refund with watcher signatures * @param payloadId_ Payload ID to mark as refund eligible - * @param nonce_ Nonce to prevent replay attacks - * @param signature_ Watcher signature + * @param nonce_ Nonce to prevent replay attacks (shared across all watchers) + * @param signatures_ Array of watcher signatures (must be exactly totalWatchers length) + * @dev Requires exactly totalWatchers signatures before marking as refund eligible. + * All watchers must use the same nonce. */ function markRefundEligible( bytes32 payloadId_, uint256 nonce_, - bytes calldata signature_ + bytes[] calldata signatures_ ) external { + if (signatures_.length != totalWatchers) revert ArrayLengthMismatch(); + PayloadFees storage fees = payloadFees[payloadId_]; if (fees.isRefundEligible) revert AlreadyMarkedRefundEligible(); if (fees.nativeFees == 0) revert NoFeesToRefund(); + bytes32 digest = keccak256( abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, nonce_) ); - address watcher = _recoverSigner(digest, signature_); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - _validateAndUseNonce(this.markRefundEligible.selector, watcher, nonce_); + + for (uint256 i = 0; i < totalWatchers; i++) { + address watcher = _validateSignature(digest, signatures_[i], WATCHER_ROLE); + _validateAndUseNonce(this.markRefundEligible.selector, watcher, nonce_); + } fees.isRefundEligible = true; - emit RefundEligibilityMarked(payloadId_, watcher); + emit RefundEligibilityMarked(payloadId_); } /** @@ -535,9 +571,7 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { ) ); - address feeUpdater = _recoverSigner(digest, signature_); - if (!_hasRole(FEE_UPDATER_ROLE, feeUpdater)) revert UnauthorizedFeeUpdater(); - + address feeUpdater = _validateSignature(digest, signature_, FEE_UPDATER_ROLE); _validateAndUseNonce(this.setMinMsgValueFeesBatch.selector, feeUpdater, nonce_); for (uint256 i = 0; i < siblingChainSlugs_.length; i++) { @@ -565,12 +599,30 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { emit SiblingConfigSet(chainSlug_, socket_, switchboard_); } + /** + * @notice Processes a single watcher signature for reverting payload + * @param payloadId_ The payload ID + * @param isReverting_ The reverting status flag + * @param watcher_ The watcher address + * @dev Checks for double signature, updates state, and sets reverting status if threshold is reached + */ + + /** + * @notice Sets reverting payload status with watcher signatures + * @param payloadId_ payload ID to mark + * @param isReverting_ reverting status flag + * @param nonce_ nonce to prevent replay attacks + * @param signatures_ Array of watcher signatures + * @dev Processes multiple payloads in a single transaction. Each payload requires totalWatchers signatures. + */ function setRevertingPayload( bytes32 payloadId_, bool isReverting_, uint256 nonce_, - bytes calldata signature_ + bytes[] memory signatures_ ) external { + if (signatures_.length != totalWatchers) revert ArrayLengthMismatch(); + bytes32 digest = keccak256( abi.encodePacked( toBytes32Format(address(this)), @@ -581,12 +633,13 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { ) ); - address watcher = _recoverSigner(digest, signature_); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - _validateAndUseNonce(this.setRevertingPayload.selector, watcher, nonce_); + for (uint256 k = 0; k < totalWatchers; k++) { + address watcher = _validateSignature(digest, signatures_[k], WATCHER_ROLE); + _validateAndUseNonce(this.setRevertingPayload.selector, watcher, nonce_); + } revertingPayloadIds[payloadId_] = isReverting_; - emit RevertingPayloadIdset(payloadId_, isReverting_); + emit RevertingPayloadIdSet(payloadId_, isReverting_); } /** @@ -671,47 +724,32 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { } /** - * @notice Assigns a transmitter address for payload execution - * @param digestParams_ The digest parameters - * @param oldTransmitter_ The old transmitter address - * @param newTransmitter_ The new transmitter address - * @param nonce_ Nonce to prevent replay attacks - * @param signature_ Watcher signature - * @dev Only callable by watcher. Used to assign the transmitter address for payload execution. + * @notice Batch assigns transmitter addresses with multiple watcher signatures + * @param params_ Array of transmitter assignment params (each requires exactly totalWatchers signatures) + * @dev Processes multiple transmitter assignments in a single transaction. Each assignment requires exactly totalWatchers signatures. */ - function assignTransmitter( - DigestParams memory digestParams_, - address oldTransmitter_, - address newTransmitter_, - uint256 nonce_, - bytes calldata signature_ - ) external { - bytes32 oldTransmitterBytes32 = toBytes32Format(oldTransmitter_); - digestParams_.transmitter = oldTransmitterBytes32; - bytes32 oldDigest = createDigest(digestParams_); - - if (payloadIdToDigest[digestParams_.payloadId] != oldDigest) revert InvalidDigest(); - - digestParams_.transmitter = toBytes32Format(newTransmitter_); - bytes32 newDigest = createDigest(digestParams_); - - address watcher = _recoverSigner( - keccak256( - abi.encodePacked( - toBytes32Format(address(this)), - chainSlug, - oldDigest, - newDigest, - nonce_ - ) - ), - signature_ + function assignTransmitter(AssignTransmitterParams memory params_) external { + if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch(); + + DigestParams memory dp = params_.digestParams; + dp.transmitter = toBytes32Format(params_.oldTransmitter); + + bytes32 oldDigest = createDigest(dp); + if (payloadIdToDigest[dp.payloadId] != oldDigest) revert InvalidDigest(); + dp.transmitter = toBytes32Format(params_.newTransmitter); + + bytes32 newDigest = createDigest(dp); + bytes32 messageHash = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, oldDigest, newDigest, params_.nonce) ); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - _validateAndUseNonce(this.assignTransmitter.selector, watcher, nonce_); - payloadIdToDigest[digestParams_.payloadId] = newDigest; - emit TransmitterAssigned(digestParams_.payloadId, newTransmitter_); + for (uint256 k = 0; k < totalWatchers; k++) { + address watcher = _validateSignature(messageHash, params_.signatures[k], WATCHER_ROLE); + _validateAndUseNonce(this.assignTransmitter.selector, watcher, params_.nonce); + } + + payloadIdToDigest[dp.payloadId] = newDigest; + emit TransmitterAssigned(dp.payloadId, params_.newTransmitter); } /** @@ -723,4 +761,16 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { transmitter = transmitter_; emit TransmitterSet(transmitter_); } + + function _extractSignatures( + bytes[] calldata signatures_, + uint256 startIndex_, + uint256 count_ + ) internal pure returns (bytes[] memory) { + bytes[] memory batchSignatures = new bytes[](count_); + for (uint256 k = 0; k < count_; k++) { + batchSignatures[k] = signatures_[startIndex_ + k]; + } + return batchSignatures; + } } diff --git a/contracts/protocol/switchboard/SwitchboardBase.sol b/contracts/protocol/switchboard/SwitchboardBase.sol index 977f4744..d0b05e50 100644 --- a/contracts/protocol/switchboard/SwitchboardBase.sol +++ b/contracts/protocol/switchboard/SwitchboardBase.sol @@ -5,7 +5,7 @@ import {ECDSA} from "solady/utils/ECDSA.sol"; import "../interfaces/ISocket.sol"; import "../interfaces/ISwitchboard.sol"; import "../../utils/AccessControl.sol"; -import {RESCUE_ROLE, GOVERNANCE_ROLE, WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; +import {RESCUE_ROLE, GOVERNANCE_ROLE, WATCHER_ROLE, FEE_UPDATER_ROLE} from "../../utils/common/AccessRoles.sol"; import {WRITE} from "../../utils/common/Constants.sol"; import {toBytes32Format} from "../../utils/common/Converters.sol"; import {createDigest} from "../../utils/common/DigestUtils.sol"; @@ -53,7 +53,7 @@ abstract contract SwitchboardBase is ISwitchboard, AccessControl { event Attested(bytes32 indexed payloadId, bytes32 indexed digest, address indexed watcher); /// @notice Event emitted when reverting payload is set - event RevertingPayloadIdset(bytes32 payloadId, bool isReverting); + event RevertingPayloadIdSet(bytes32 payloadId, bool isReverting); /// @notice Event emitted when default deadline is set event DefaultDeadlineIntervalSet(uint256 defaultDeadlineInterval); @@ -130,6 +130,39 @@ abstract contract SwitchboardBase is ISwitchboard, AccessControl { signer = ECDSA.recover(digest, signature_); } + /** + * @notice Validates a single signature and recovers signer + * @param messageHash_ The message hash that was signed + * @param signature_ The signature bytes + * @param requiredRole_ The role that the signer must have (bytes32(0) to skip role check) + * @return signer The recovered signer address + * @dev Reverts if signature is invalid or if signer doesn't have the required role. + * Uses Ethereum signed message format. + */ + function _validateSignature( + bytes32 messageHash_, + bytes memory signature_, + bytes32 requiredRole_ + ) internal view returns (address signer) { + signer = _recoverSigner(messageHash_, signature_); + if (requiredRole_ != bytes32(0) && !_hasRole(requiredRole_, signer)) { + if (requiredRole_ == WATCHER_ROLE) revert WatcherNotFound(); + if (requiredRole_ == FEE_UPDATER_ROLE) revert UnauthorizedFeeUpdater(); + revert RoleNotAuthorized(requiredRole_); + } + } + + function _validateBatchSignatures( + bytes32 messageHash_, + bytes[] memory signatures_, + bytes32 requiredRole_ + ) internal view returns (address[] memory signers) { + signers = new address[](signatures_.length); + for (uint256 i = 0; i < signatures_.length; i++) { + signers[i] = _validateSignature(messageHash_, signatures_[i], requiredRole_); + } + } + // --- Rescue Functions --- /** diff --git a/contracts/utils/common/DigestUtils.sol b/contracts/utils/common/DigestUtils.sol index 3306df32..11e76fdc 100644 --- a/contracts/utils/common/DigestUtils.sol +++ b/contracts/utils/common/DigestUtils.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.28; -import { DigestParams } from "./Structs.sol"; -import { toBytes32Format } from "./Converters.sol"; +import {DigestParams} from "./Structs.sol"; +import {toBytes32Format} from "./Converters.sol"; /// @notice Creates the digest for the payload execution /// @param digestParams_ The digest parameters diff --git a/contracts/utils/common/Errors.sol b/contracts/utils/common/Errors.sol index 59a5bde8..81dde0e1 100644 --- a/contracts/utils/common/Errors.sol +++ b/contracts/utils/common/Errors.sol @@ -168,3 +168,6 @@ error WatcherFound(); /// @notice Thrown when digest does not match stored digest error InvalidDigest(); + +/// @notice Thrown when role is not authorized +error RoleNotAuthorized(bytes32 role); diff --git a/test/protocol/Socket.t.sol b/test/protocol/Socket.t.sol index 988af43e..4fe38644 100644 --- a/test/protocol/Socket.t.sol +++ b/test/protocol/Socket.t.sol @@ -316,14 +316,16 @@ contract SocketTestBase is Test, Utils { address(this), abi.encodeWithSelector( WritePrecompile.initialize.selector, - address(0), address(0), 1, 1 + address(0), + address(0), + 1, + 1 ) ); writePrecompile = WritePrecompile(proxy); return writePrecompile; } - function _createExecutionParams() internal view returns (ExecutionParams memory) { bytes32 payloadId = createPayloadId( TEST_CHAIN_SLUG, // source chain slug @@ -1116,7 +1118,7 @@ contract SocketUtilsTest is SocketTestBase { 14323, // origin chain slug - evmx 1, // origin watcher id CHAIN_SLUG_SOLANA_MAINNET, // verification chain slug (matches socket) - 1, // verification switchboard id (matches plug's switchboard) + 1, // verification switchboard id (matches plug's switchboard) 600 // pointer / counter ); @@ -1132,7 +1134,8 @@ contract SocketUtilsTest is SocketTestBase { address appGateway = address(0xCDB5fE8572725B20A2C0Db85DDb0D025bCC16f86); // real value taken from 07-calculate-digest.ts in Solana Socket repo test - bytes memory payloadPacked = hex"0d2d692c8c7f3ff61408499c3eb4865321405fb1c9bfc5014a63672453973780cb33f7992e094d6c65e0e95cf54e70c3470840e69d739dfcfcf2e1805fc913d1e8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a93339e12fb69289a640420f0000000000"; + bytes + memory payloadPacked = hex"0d2d692c8c7f3ff61408499c3eb4865321405fb1c9bfc5014a63672453973780cb33f7992e094d6c65e0e95cf54e70c3470840e69d739dfcfcf2e1805fc913d1e8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a93339e12fb69289a640420f0000000000"; DigestParams memory digestParams = DigestParams({ socket: socketSolana, @@ -1145,20 +1148,23 @@ contract SocketUtilsTest is SocketTestBase { payload: payloadPacked, target: targetSolana, source: abi.encodePacked(toBytes32Format(appGateway)), - prevBatchDigestHash: bytes32(0x614bf23210ee3f9fd714634e1ea66f52cea9e6177140d533a981d511ebd785d3), + prevBatchDigestHash: bytes32( + 0x614bf23210ee3f9fd714634e1ea66f52cea9e6177140d533a981d511ebd785d3 + ), extraData: bytes("") }); // console.log("Source:"); // console.logBytes(abi.encodePacked(toBytes32Format(appGateway))); - bytes32 digest = writePrecompile.getDigest(digestParams); // console.log("Digest solana:"); // console.logBytes32(digest); - assertTrue(digest == bytes32(0x6842eaeba430c89bd05048c9f63394a6c2b0ca5621ae636fef29d4b5293dbc6f)); + assertTrue( + digest == bytes32(0x6842eaeba430c89bd05048c9f63394a6c2b0ca5621ae636fef29d4b5293dbc6f) + ); } } diff --git a/test/protocol/switchboard/EVMxSwitchboard.t.sol b/test/protocol/switchboard/EVMxSwitchboard.t.sol index ae117217..f2b393c2 100644 --- a/test/protocol/switchboard/EVMxSwitchboard.t.sol +++ b/test/protocol/switchboard/EVMxSwitchboard.t.sol @@ -29,8 +29,10 @@ contract EVMxSwitchboardTestBase is Test, Utils { address plugOwner = address(0x2000); address watcher = address(0x3000); - // Private key for watcher signing + // Private keys for watcher signing uint256 watcherPrivateKey = 0x1111111111111111111111111111111111111111111111111111111111111111; + uint256 watcher2PrivateKey = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 watcher3PrivateKey = 0x3333333333333333333333333333333333333333333333333333333333333333; Socket socket; EVMxSwitchboard evmxSwitchboard; @@ -41,6 +43,8 @@ contract EVMxSwitchboardTestBase is Test, Utils { ExecutionParams public executionParams; TransmissionParams public transmissionParams; + uint256 nonce = 1; + function setUp() public virtual { // Deploy Socket socket = new Socket(CHAIN_SLUG, owner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); @@ -187,6 +191,31 @@ contract EVMxSwitchboardTestBase is Test, Utils { return vm.addr(watcherPrivateKey); } + /** + * @dev Helper to get watcher2 address from private key + */ + function getWatcher2Address() public view returns (address) { + return vm.addr(watcher2PrivateKey); + } + + /** + * @dev Helper to get watcher3 address from private key + */ + function getWatcher3Address() public view returns (address) { + return vm.addr(watcher3PrivateKey); + } + + /** + * @dev Helper to get all watcher addresses + */ + function getAllWatcherAddresses() public view returns (address[] memory) { + address[] memory watchers = new address[](3); + watchers[0] = getWatcherAddress(); + watchers[1] = getWatcher2Address(); + watchers[2] = getWatcher3Address(); + return watchers; + } + /** * @dev Helper to create signature for attest function */ @@ -205,6 +234,50 @@ contract EVMxSwitchboardTestBase is Test, Utils { return createSignature(signatureDigest, watcherPrivateKey); } + /** + * @dev Helper to create batch signatures for setRevertingPayload + */ + function _createSetRevertingPayloadSignatures( + bytes32 payloadId_, + bool isReverting_, + uint256 nonce_ + ) internal view returns (bytes[] memory) { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId_, + isReverting_, + nonce_ + ) + ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, watcherPrivateKey); + return signatures; + } + + /** + * @dev Helper to create batch signatures for assignTransmitter + */ + function _createAssignTransmitterBatchSignatures( + bytes32 oldDigest_, + bytes32 newDigest_ + ) internal view returns (bytes[] memory) { + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + oldDigest_, + newDigest_ + ) + ); + bytes[] memory signatures = new bytes[](3); + signatures[0] = createSignature(messageHash, watcherPrivateKey); + signatures[1] = createSignature(messageHash, watcher2PrivateKey); + signatures[2] = createSignature(messageHash, watcher3PrivateKey); + return signatures; + } + /** * @dev Helper to setup payload for assignTransmitter tests * @return payloadId The created payload ID @@ -306,6 +379,13 @@ contract EVMxSwitchboardTestBase is Test, Utils { ); signature = createSignature(signatureDigest, watcherPrivateKey); } + + function addWatchers() internal { + hoax(owner); + evmxSwitchboard.grantWatcherRole(getWatcher2Address()); + hoax(owner); + evmxSwitchboard.grantWatcherRole(getWatcher3Address()); + } } /** @@ -596,7 +676,7 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { digest ) ); - uint256 invalidPrivateKey = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 invalidPrivateKey = 0x4444444444444444444444444444444444444444444444444444444444444444; bytes memory invalidSignature = createSignature(signatureDigest, invalidPrivateKey); vm.prank(vm.addr(invalidPrivateKey)); @@ -740,33 +820,33 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { bytes32 payloadId = bytes32(uint256(0x1234)); bool isReverting = true; uint256 nonce = 1; - bytes32 digest = keccak256( - abi.encodePacked( - toBytes32Format(address(evmxSwitchboard)), - CHAIN_SLUG, - payloadId, - isReverting, - nonce - ) + bytes[] memory signatures = _createSetRevertingPayloadSignatures( + payloadId, + isReverting, + nonce ); - bytes memory signature = createSignature(digest, watcherPrivateKey); vm.expectEmit(true, false, false, true); - emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); + emit SwitchboardBase.RevertingPayloadIdSet(payloadId, isReverting); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); + assertTrue(evmxSwitchboard.revertingPayloadIds(payloadId)); + } - vm.prank(getWatcherAddress()); - evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + function test_SetRevertingPayload_WrongSignatureCount_Reverts() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + bytes[] memory signatures = new bytes[](2); // Wrong count - should be totalWatchers - // Verify it was set (check via allowPayload or directly if there's a getter) - // Note: revertingPayloadIds is internal, so we can't directly check it - // But we can verify the event was emitted + vm.expectRevert(ArrayLengthMismatch.selector); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); } - function test_SetRevertingPayload_OnlyOwner() public { + function test_SetRevertingPayload_InvalidWatcher_Reverts() public { bytes32 payloadId = bytes32(uint256(0x1234)); bool isReverting = true; - uint256 nonce = 1; + bytes32 digest = keccak256( abi.encodePacked( toBytes32Format(address(evmxSwitchboard)), @@ -776,10 +856,11 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { nonce ) ); - bytes memory signature = createSignature(digest, uint256(0x1234)); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, uint256(0x1234)); // Invalid watcher - vm.expectRevert(); - evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + vm.expectRevert(WatcherNotFound.selector); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); } // ============================================ @@ -999,11 +1080,12 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { function test_grantWatcherRole_Success() public { address newWatcher = address(0x5000); + uint256 totalBefore = evmxSwitchboard.totalWatchers(); vm.prank(owner); evmxSwitchboard.grantWatcherRole(newWatcher); assertTrue(evmxSwitchboard.hasRole(WATCHER_ROLE, newWatcher)); - assertEq(evmxSwitchboard.totalWatchers(), 2); // 1 from setUp + 1 new + assertEq(evmxSwitchboard.totalWatchers(), totalBefore + 1); } function test_grantWatcherRole_WatcherFound_Reverts() public { @@ -1081,44 +1163,55 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { bytes32 payloadId = bytes32(uint256(0x1234)); bool isReverting = true; uint256 nonce = 1; - bytes32 digest = keccak256( - abi.encodePacked( - toBytes32Format(address(evmxSwitchboard)), - CHAIN_SLUG, - payloadId, - isReverting, - nonce - ) + + bytes[] memory signatures = _createSetRevertingPayloadSignatures( + payloadId, + isReverting, + nonce ); - bytes memory signature = createSignature(digest, watcherPrivateKey); // First call succeeds - vm.prank(getWatcherAddress()); - evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); - // Second call with same nonce should revert - vm.prank(getWatcherAddress()); + // Second call with same nonce should revert (nonce already used by first watcher) vm.expectRevert(NonceAlreadyUsed.selector); - evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); } // ============================================ // MISSING TESTS - Attest with Multiple Watchers // ============================================ - function test_Attest_ReachesThreshold_SetsIsValid() public { - // Grant watcher role to 2 more watchers (total 3) - // Use addresses derived from private keys to match signatures - uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; - uint256 watcher3Key = 0x3333333333333333333333333333333333333333333333333333333333333333; - address watcher2 = vm.addr(watcher2Key); - address watcher3 = vm.addr(watcher3Key); + function test_BatchAttest_Success() public { + addWatchers(); + assertEq(evmxSwitchboard.totalWatchers(), 3); + bytes32 digest = keccak256(abi.encode("test payload")); + bytes32 payloadId = bytes32(uint256(0x1234)); - vm.startPrank(owner); - evmxSwitchboard.grantWatcherRole(watcher2); - evmxSwitchboard.grantWatcherRole(watcher3); - vm.stopPrank(); + bytes[] memory signatures = new bytes[](3); + bytes32 signatureDigest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId, + digest + ) + ); + signatures[0] = createSignature(signatureDigest, watcherPrivateKey); + signatures[1] = createSignature(signatureDigest, watcher2PrivateKey); + signatures[2] = createSignature(signatureDigest, watcher3PrivateKey); + + vm.expectEmit(true, false, true, true); + emit SwitchboardBase.Attested(payloadId, digest, getWatcherAddress()); + + evmxSwitchboard.batchAttest(payloadId, digest, signatures); + assertTrue(evmxSwitchboard.isValid(payloadId, digest)); + assertEq(evmxSwitchboard.attestations(payloadId, digest), 3); + } + + function test_Attest_ReachesThreshold_SetsIsValid() public { + addWatchers(); assertEq(evmxSwitchboard.totalWatchers(), 3); bytes32 digest = keccak256(abi.encode("test payload")); @@ -1139,14 +1232,14 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { digest ) ); - bytes memory signature2 = createSignature(signatureDigest, watcher2Key); - vm.prank(watcher2); + bytes memory signature2 = createSignature(signatureDigest, watcher2PrivateKey); + vm.prank(getWatcher2Address()); evmxSwitchboard.attest(payloadId, digest, signature2); assertFalse(evmxSwitchboard.isValid(payloadId, digest)); // Still not enough // Third attestation - should set isValid to true - bytes memory signature3 = createSignature(signatureDigest, watcher3Key); - vm.prank(watcher3); + bytes memory signature3 = createSignature(signatureDigest, watcher3PrivateKey); + vm.prank(getWatcher3Address()); evmxSwitchboard.attest(payloadId, digest, signature3); assertTrue(evmxSwitchboard.isValid(payloadId, digest)); // Now valid! assertEq(evmxSwitchboard.attestations(payloadId, digest), 3); @@ -1157,7 +1250,6 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { // ============================================ function test_AssignTransmitter_Success() public { - // Setup payload ( bytes32 payloadId, MockPlug triggerPlug, @@ -1165,55 +1257,89 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { bytes memory payload ) = _setupPayloadForAssignTransmitter(); - // Get the stored digest bytes32 storedDigest = evmxSwitchboard.payloadIdToDigest(payloadId); - - address oldTransmitter = address(0); // Initial transmitter is address(0) address newTransmitter = address(0x5000); - uint256 nonce = 1; - - // Create digest params with old transmitter DigestParams memory digestParams = _createAssignTransmitterDigestParams( payloadId, address(triggerPlug), appGatewayId, payload, - oldTransmitter + address(0) ); - // Verify old digest matches stored digest bytes32 oldDigest = createDigest(digestParams); assertEq(oldDigest, storedDigest, "Old digest should match stored digest"); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce - ); - - // Create new digest for verification digestParams.transmitter = toBytes32Format(newTransmitter); bytes32 newDigest = createDigest(digestParams); - digestParams.transmitter = toBytes32Format(oldTransmitter); - // Expect event (2 indexed parameters: payloadId and transmitter) + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + oldDigest, + newDigest, + nonce + ) + ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); + vm.expectEmit(true, true, false, true); emit EVMxSwitchboard.TransmitterAssigned(payloadId, newTransmitter); - // Call assignTransmitter - vm.prank(getWatcherAddress()); - evmxSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature + digestParams.transmitter = toBytes32Format(address(0)); + _callAssignTransmitter(digestParams, address(0), newTransmitter, 1, signatures); + + assertEq( + evmxSwitchboard.payloadIdToDigest(payloadId), + newDigest, + "Digest should be updated with new transmitter" ); + } + + function test_AssignTransmitter_WrongSignatureCount_Reverts() public { + ( + bytes32 payloadId, + MockPlug triggerPlug, + bytes32 appGatewayId, + bytes memory payload + ) = _setupPayloadForAssignTransmitter(); + + DigestParams memory digestParams = _createAssignTransmitterDigestParams( + payloadId, + address(triggerPlug), + appGatewayId, + payload, + address(0) + ); + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(address(0x5000)); + bytes32 newDigest = createDigest(digestParams); + bytes[] memory signatures = new bytes[](2); // Wrong count - should be 3 - // Verify digest was updated - bytes32 updatedDigest = evmxSwitchboard.payloadIdToDigest(payloadId); - assertEq(updatedDigest, newDigest, "Digest should be updated with new transmitter"); + digestParams.transmitter = toBytes32Format(address(0)); + vm.expectRevert(ArrayLengthMismatch.selector); + _callAssignTransmitter(digestParams, address(0), address(0x5000), 1, signatures); + } + + function _callAssignTransmitter( + DigestParams memory digestParams_, + address oldTransmitter_, + address newTransmitter_, + uint256 nonce_, + bytes[] memory signatures_ + ) internal { + EVMxSwitchboard.AssignTransmitterParams memory params = EVMxSwitchboard + .AssignTransmitterParams({ + digestParams: digestParams_, + oldTransmitter: oldTransmitter_, + newTransmitter: newTransmitter_, + nonce: nonce_, + signatures: signatures_ + }); + + evmxSwitchboard.assignTransmitter(params); } function test_AssignTransmitter_InvalidOldDigest_Reverts() public { @@ -1239,27 +1365,34 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { wrongOldTransmitter ); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(newTransmitter); + bytes32 newDigest = createDigest(digestParams); + + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + oldDigest, + newDigest, + nonce + ) ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); - // Should revert because old digest doesn't match stored digest - vm.prank(getWatcherAddress()); + digestParams.transmitter = toBytes32Format(wrongOldTransmitter); vm.expectRevert(InvalidDigest.selector); - evmxSwitchboard.assignTransmitter( + _callAssignTransmitter( digestParams, wrongOldTransmitter, newTransmitter, nonce, - signature + signatures ); } function test_AssignTransmitter_InvalidWatcher_Reverts() public { - // Setup payload ( bytes32 payloadId, MockPlug triggerPlug, @@ -1267,58 +1400,37 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { bytes memory payload ) = _setupPayloadForAssignTransmitter(); - address oldTransmitter = address(0); - address newTransmitter = address(0x5000); - uint256 nonce = 1; - - // Create digest params with old transmitter DigestParams memory digestParams = _createAssignTransmitterDigestParams( payloadId, address(triggerPlug), appGatewayId, payload, - oldTransmitter + address(0) ); - // Create signature with non-watcher private key - uint256 nonWatcherKey = 0x2222222222222222222222222222222222222222222222222222222222222222; - address nonWatcher = vm.addr(nonWatcherKey); - - // Create old digest with old transmitter bytes32 oldDigest = createDigest(digestParams); - - // Create new digest with new transmitter - digestParams.transmitter = toBytes32Format(newTransmitter); + digestParams.transmitter = toBytes32Format(address(0x5000)); bytes32 newDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(address(0)); - // Create signature digest with both old and new digests with non-watcher key - bytes32 signatureDigest = keccak256( + uint256 nonWatcherKey = 0x9999999999999999999999999999999999999999999999999999999999999999; + bytes32 messageHash = keccak256( abi.encodePacked( toBytes32Format(address(evmxSwitchboard)), CHAIN_SLUG, oldDigest, - newDigest + newDigest, + nonce ) ); - bytes memory signature = createSignature(signatureDigest, nonWatcherKey); - - // Reset transmitter for the function call - digestParams.transmitter = toBytes32Format(oldTransmitter); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, nonWatcherKey); - // Should revert because signer is not a watcher - vm.prank(nonWatcher); vm.expectRevert(WatcherNotFound.selector); - evmxSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature - ); + _callAssignTransmitter(digestParams, address(0), address(0x5000), 1, signatures); } function test_AssignTransmitter_NonceAlreadyUsed_Reverts() public { - // Setup payload ( bytes32 payloadId, MockPlug triggerPlug, @@ -1326,65 +1438,55 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { bytes memory payload ) = _setupPayloadForAssignTransmitter(); - address oldTransmitter = address(0); - address newTransmitter = address(0x5000); - uint256 nonce = 1; - - // Create digest params with old transmitter DigestParams memory digestParams = _createAssignTransmitterDigestParams( payloadId, address(triggerPlug), appGatewayId, payload, - oldTransmitter + address(0) ); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce - ); + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(address(0x5000)); + bytes32 newDigest = createDigest(digestParams); - // First call succeeds - vm.prank(getWatcherAddress()); - evmxSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + oldDigest, + newDigest, + nonce + ) ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); - // Second call with same nonce should revert - // Need to update oldTransmitter to the new one since we already assigned it - address updatedOldTransmitter = newTransmitter; - address anotherNewTransmitter = address(0x6000); + digestParams.transmitter = toBytes32Format(address(0)); + _callAssignTransmitter(digestParams, address(0), address(0x5000), 1, signatures); - // Create new digest params with updated old transmitter - DigestParams memory updatedDigestParams = _createAssignTransmitterDigestParams( - payloadId, - address(triggerPlug), - appGatewayId, - payload, - updatedOldTransmitter - ); - - // Create signature for the new assignment - bytes memory signature2 = _createAssignTransmitterSignature( - updatedDigestParams, - anotherNewTransmitter, - nonce - ); + vm.expectRevert(InvalidDigest.selector); + _callAssignTransmitter(digestParams, address(0), address(0x5000), 1, signatures); + } - vm.prank(getWatcherAddress()); - vm.expectRevert(NonceAlreadyUsed.selector); - evmxSwitchboard.assignTransmitter( - updatedDigestParams, - updatedOldTransmitter, - anotherNewTransmitter, - nonce, // Same nonce - should revert - signature2 + function _setupSecondPayloadForAssignTransmitter() internal returns (bytes32 payloadId2) { + MockPlug triggerPlug2 = _createTriggerPlug(); + bytes32 appGatewayId2 = toBytes32Format(address(0x5678)); + bytes memory plugConfig2 = abi.encode(appGatewayId2); + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(triggerPlug2), plugConfig2); + bytes memory payload2 = abi.encode("test2"); + EVMxOverrides memory overridesParams = EVMxOverrides({ + gasLimit: 0, + deadline: 0, + maxFees: 0 + }); + bytes memory overrides = abi.encode(overridesParams); + vm.prank(address(socket)); + payloadId2 = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug2), + payload2, + overrides ); } } diff --git a/test/protocol/switchboard/MessageSwitchboard.t.sol b/test/protocol/switchboard/MessageSwitchboard.t.sol index d90aa293..af8aa9f8 100644 --- a/test/protocol/switchboard/MessageSwitchboard.t.sol +++ b/test/protocol/switchboard/MessageSwitchboard.t.sol @@ -27,6 +27,8 @@ contract MessageSwitchboardTest is Test, Utils { // Private keys for signing uint256 watcherPrivateKey = 0x1111111111111111111111111111111111111111111111111111111111111111; + uint256 watcher2PrivateKey = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 watcher3PrivateKey = 0x3333333333333333333333333333333333333333333333333333333333333333; uint256 feeUpdaterPrivateKey = 0x5555555555555555555555555555555555555555555555555555555555555555; @@ -64,6 +66,16 @@ contract MessageSwitchboardTest is Test, Utils { return vm.addr(0x1111111111111111111111111111111111111111111111111111111111111111); } + // Helper to get watcher2 address + function getWatcher2Address() public pure returns (address) { + return vm.addr(0x2222222222222222222222222222222222222222222222222222222222222222); + } + + // Helper to get watcher3 address + function getWatcher3Address() public pure returns (address) { + return vm.addr(0x3333333333333333333333333333333333333333333333333333333333333333); + } + // Helper to get fee updater address from private key function getFeeUpdaterAddress() public view returns (address) { return vm.addr(feeUpdaterPrivateKey); @@ -366,6 +378,65 @@ contract MessageSwitchboardTest is Test, Utils { return createSignature(digest, watcherPrivateKey); } + function _createMarkRefundEligibleBatchSignatures( + bytes32 payloadId, + uint256 nonce + ) internal view returns (bytes[] memory) { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + nonce + ) + ); + bytes[] memory signatures = new bytes[](3); + signatures[0] = createSignature(digest, watcherPrivateKey); + signatures[1] = createSignature(digest, watcher2PrivateKey); + signatures[2] = createSignature(digest, watcher3PrivateKey); + return signatures; + } + + function _createSetRevertingPayloadBatchSignatures( + bytes32 payloadId, + bool isReverting, + uint256 nonce + ) internal view returns (bytes[] memory) { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + isReverting, + nonce + ) + ); + bytes[] memory signatures = new bytes[](3); + signatures[0] = createSignature(digest, watcherPrivateKey); + signatures[1] = createSignature(digest, watcher2PrivateKey); + signatures[2] = createSignature(digest, watcher3PrivateKey); + return signatures; + } + + function _createAssignTransmitterBatchSignatures( + bytes32 oldDigest, + bytes32 newDigest + ) internal view returns (bytes[] memory) { + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + oldDigest, + newDigest + ) + ); + bytes[] memory signatures = new bytes[](3); + signatures[0] = createSignature(messageHash, watcherPrivateKey); + signatures[1] = createSignature(messageHash, watcher2PrivateKey); + signatures[2] = createSignature(messageHash, watcher3PrivateKey); + return signatures; + } + /** * @dev Create watcher signature for a given payload ID (backwards compatibility, uses nonce 0) * @param payloadId The payload ID to sign @@ -973,37 +1044,48 @@ contract MessageSwitchboardTest is Test, Utils { // Mark eligible with nonce uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); vm.expectEmit(true, true, false, false); - emit MessageSwitchboard.RefundEligibilityMarked(payloadId, getWatcherAddress()); + emit MessageSwitchboard.RefundEligibilityMarked(payloadId); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); // Verify marked eligible (, , bool isEligible, , ) = messageSwitchboard.payloadFees(payloadId); assertTrue(isEligible); - // Verify nonce was used + // Verify nonces were used uint256 namespacedNonce = uint256( keccak256(abi.encodePacked(messageSwitchboard.markRefundEligible.selector, nonce)) ); assertTrue(messageSwitchboard.usedNonces(getWatcherAddress(), namespacedNonce)); + // Only the single configured watcher should have its nonce used + } + + function test_markRefundEligible_WrongSignatureCount_Reverts() public { + _setupCompleteNative(); + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + uint256 nonce = 1; + bytes[] memory signatures = new bytes[](2); // Wrong count - should be 3 + + vm.expectRevert(ArrayLengthMismatch.selector); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); } function test_markRefundEligible_NoFeesToRefund_Reverts() public { // Create a non-existent payloadId (one that was never created) bytes32 payloadId = bytes32(uint256(0x9999)); - // Create valid watcher signature (this will pass watcher check) + // Create valid watcher signatures (this will pass watcher check) uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); // Should revert with NoFeesToRefund because payload doesn't exist - vm.prank(getWatcherAddress()); vm.expectRevert(NoFeesToRefund.selector); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); } function test_markRefundEligible_AlreadyMarkedRefundEligible_Reverts() public { @@ -1013,16 +1095,16 @@ contract MessageSwitchboardTest is Test, Utils { // Mark eligible first time uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); // Try to mark eligible again with different nonce - should revert (already marked) uint256 nonce2 = 2; - bytes memory signature2 = _createWatcherSignature(payloadId, nonce2); - vm.prank(getWatcherAddress()); + bytes[] memory signatures2 = new bytes[](1); + signatures2[0] = _createWatcherSignature(payloadId, nonce2); vm.expectRevert(AlreadyMarkedRefundEligible.selector); - messageSwitchboard.markRefundEligible(payloadId, nonce2, signature2); + messageSwitchboard.markRefundEligible(payloadId, nonce2, signatures2); } function test_markRefundEligible_NonceAlreadyUsed_Reverts() public { @@ -1033,15 +1115,15 @@ contract MessageSwitchboardTest is Test, Utils { // Mark first payload eligible with nonce uint256 nonce = 1; - bytes memory signature1 = _createWatcherSignature(payloadId1, nonce); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId1, nonce, signature1); + bytes[] memory signatures1 = new bytes[](1); + signatures1[0] = _createWatcherSignature(payloadId1, nonce); + messageSwitchboard.markRefundEligible(payloadId1, nonce, signatures1); // Try to use the same nonce again on a different payload - should revert with NonceAlreadyUsed - bytes memory signature2 = _createWatcherSignature(payloadId2, nonce); - vm.prank(getWatcherAddress()); + bytes[] memory signatures2 = new bytes[](1); + signatures2[0] = _createWatcherSignature(payloadId2, nonce); vm.expectRevert(NonceAlreadyUsed.selector); - messageSwitchboard.markRefundEligible(payloadId2, nonce, signature2); + messageSwitchboard.markRefundEligible(payloadId2, nonce, signatures2); } function test_markRefundEligible_AfterRefund_Reverts() public { @@ -1051,9 +1133,9 @@ contract MessageSwitchboardTest is Test, Utils { // Mark eligible and refund uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); vm.deal(address(messageSwitchboard), MIN_FEES); vm.prank(refundAddress); @@ -1062,10 +1144,10 @@ contract MessageSwitchboardTest is Test, Utils { // After refund, isRefundEligible is still true, so trying to mark eligible again // will revert with AlreadyMarkedRefundEligible (line 429), not AlreadyRefunded (line 430) uint256 nonce2 = 2; - bytes memory signature2 = _createWatcherSignature(payloadId, nonce2); - vm.prank(getWatcherAddress()); + bytes[] memory signatures2 = new bytes[](1); + signatures2[0] = _createWatcherSignature(payloadId, nonce2); vm.expectRevert(AlreadyMarkedRefundEligible.selector); - messageSwitchboard.markRefundEligible(payloadId, nonce2, signature2); + messageSwitchboard.markRefundEligible(payloadId, nonce2, signatures2); } function test_markRefundEligible_InvalidWatcher_Reverts() public { @@ -1086,12 +1168,12 @@ contract MessageSwitchboardTest is Test, Utils { nonce ) ); - bytes memory signature = createSignature(digest, nonWatcherPrivateKey); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, nonWatcherPrivateKey); // Should revert with WatcherNotFound at line 460 - vm.prank(nonWatcher); vm.expectRevert(WatcherNotFound.selector); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); } function test_refund_Success() public { @@ -1102,9 +1184,9 @@ contract MessageSwitchboardTest is Test, Utils { // Mark eligible uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); // Refund uint256 balanceBefore = refundAddress.balance; @@ -1138,9 +1220,9 @@ contract MessageSwitchboardTest is Test, Utils { // Mark eligible and refund once uint256 nonce = 1; - bytes memory signature = _createWatcherSignature(payloadId, nonce); - vm.prank(getWatcherAddress()); - messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + bytes[] memory signatures = new bytes[](1); + signatures[0] = _createWatcherSignature(payloadId, nonce); + messageSwitchboard.markRefundEligible(payloadId, nonce, signatures); vm.deal(address(messageSwitchboard), MIN_FEES); vm.prank(refundAddress); @@ -1421,7 +1503,6 @@ contract MessageSwitchboardTest is Test, Utils { bytes32 payloadId = bytes32(uint256(0x1234)); bool isReverting = true; uint256 nonce = 1; - bytes32 digest = keccak256( abi.encodePacked( toBytes32Format(address(messageSwitchboard)), @@ -1431,35 +1512,53 @@ contract MessageSwitchboardTest is Test, Utils { nonce ) ); - bytes memory signature = createSignature(digest, watcherPrivateKey); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, watcherPrivateKey); vm.expectEmit(true, true, false, true); - emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); + emit SwitchboardBase.RevertingPayloadIdSet(payloadId, isReverting); - vm.prank(getWatcherAddress()); - messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); assertTrue(messageSwitchboard.revertingPayloadIds(payloadId)); } - function test_setRevertingPayload_NotOwner_Reverts() public { + function test_setRevertingPayload_WrongSignatureCount_Reverts() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + bytes[] memory signatures = new bytes[](2); // Wrong count - should be 3 + + vm.expectRevert(ArrayLengthMismatch.selector); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); + } + + function test_setRevertingPayload_InvalidWatcher_Reverts() public { bytes32 payloadId = bytes32(uint256(0x1234)); bool isReverting = true; uint256 nonce = 1; bytes32 digest = keccak256( - abi.encodePacked(address(messageSwitchboard), SRC_CHAIN, payloadId, isReverting, nonce) + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + isReverting, + nonce + ) ); - bytes memory signature = createSignature(digest, feeUpdaterPrivateKey); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, feeUpdaterPrivateKey); - vm.expectRevert(); - messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + vm.expectRevert(WatcherNotFound.selector); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); } function test_setRevertingPayload_SetToFalse() public { bytes32 payloadId = bytes32(uint256(0x1234)); uint256 nonce = 1; bool isReverting = true; + bytes32 digest = keccak256( abi.encodePacked( toBytes32Format(address(messageSwitchboard)), @@ -1469,12 +1568,11 @@ contract MessageSwitchboardTest is Test, Utils { nonce ) ); - - bytes memory signature = createSignature(digest, watcherPrivateKey); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(digest, watcherPrivateKey); // First set to true - vm.prank(getWatcherAddress()); - messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); assertTrue(messageSwitchboard.revertingPayloadIds(payloadId)); nonce++; @@ -1488,15 +1586,12 @@ contract MessageSwitchboardTest is Test, Utils { nonce ) ); - - signature = createSignature(digest, watcherPrivateKey); + signatures[0] = createSignature(digest, watcherPrivateKey); // Then set to false vm.expectEmit(true, false, false, true); - emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); - - vm.prank(getWatcherAddress()); - messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + emit SwitchboardBase.RevertingPayloadIdSet(payloadId, isReverting); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signatures); assertFalse(messageSwitchboard.revertingPayloadIds(payloadId)); } @@ -1881,6 +1976,7 @@ contract MessageSwitchboardTest is Test, Utils { function test_grantWatcherRole_Success() public { address newWatcher = address(0x5000); + uint256 totalBefore = messageSwitchboard.totalWatchers(); vm.startPrank(owner); messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); vm.stopPrank(); @@ -1888,7 +1984,7 @@ contract MessageSwitchboardTest is Test, Utils { vm.prank(owner); messageSwitchboard.grantWatcherRole(newWatcher); assertTrue(messageSwitchboard.hasRole(WATCHER_ROLE, newWatcher)); - assertEq(messageSwitchboard.totalWatchers(), 2); // 1 from setUp + 1 new + assertEq(messageSwitchboard.totalWatchers(), totalBefore + 1); } function test_grantWatcherRole_WatcherFound_Reverts() public { @@ -2447,37 +2543,60 @@ contract MessageSwitchboardTest is Test, Utils { bytes32 oldDigest = createDigest(digestParams); assertEq(oldDigest, storedDigest, "Old digest should match stored digest"); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce - ); - // Create new digest for verification digestParams.transmitter = toBytes32Format(newTransmitter); bytes32 newDigest = createDigest(digestParams); digestParams.transmitter = toBytes32Format(oldTransmitter); + // Create single watcher signature + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + oldDigest, + newDigest, + nonce + ) + ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); + // Expect event (2 indexed parameters: payloadId and transmitter) vm.expectEmit(true, true, false, true); emit MessageSwitchboard.TransmitterAssigned(payloadId, newTransmitter); - // Call assignTransmitter - vm.prank(getWatcherAddress()); - messageSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature - ); + _callAssignTransmitter(digestParams, oldTransmitter, newTransmitter, nonce, signatures); // Verify digest was updated bytes32 updatedDigest = messageSwitchboard.payloadIdToDigest(payloadId); assertEq(updatedDigest, newDigest, "Digest should be updated with new transmitter"); } + function test_AssignTransmitter_WrongSignatureCount_Reverts() public { + ( + bytes32 payloadId, + MockPlug triggerPlug, + bytes32 appGatewayId, + bytes memory payload + ) = _setupPayloadForAssignTransmitter(); + + DigestParams memory digestParams = _createAssignTransmitterDigestParams( + payloadId, + address(triggerPlug), + appGatewayId, + payload, + address(0) + ); + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(address(0x5000)); + bytes32 newDigest = createDigest(digestParams); + bytes[] memory signatures = new bytes[](2); // Wrong count - should be 3 + + digestParams.transmitter = toBytes32Format(address(0)); + vm.expectRevert(ArrayLengthMismatch.selector); + _callAssignTransmitter(digestParams, address(0), address(0x5000), 1, signatures); + } + function test_AssignTransmitter_InvalidOldDigest_Reverts() public { // Setup payload ( @@ -2501,22 +2620,32 @@ contract MessageSwitchboardTest is Test, Utils { wrongOldTransmitter ); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(newTransmitter); + bytes32 newDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(wrongOldTransmitter); + + // Create single watcher signature + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + oldDigest, + newDigest, + nonce + ) ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); // Should revert because old digest doesn't match stored digest - vm.prank(getWatcherAddress()); vm.expectRevert(InvalidDigest.selector); - messageSwitchboard.assignTransmitter( + _callAssignTransmitter( digestParams, wrongOldTransmitter, newTransmitter, nonce, - signature + signatures ); } @@ -2544,7 +2673,6 @@ contract MessageSwitchboardTest is Test, Utils { // Create signature with non-watcher private key uint256 nonWatcherKey = 0x2222222222222222222222222222222222222222222222222222222222222222; - address nonWatcher = vm.addr(nonWatcherKey); // Create old digest with old transmitter bytes32 oldDigest = createDigest(digestParams); @@ -2554,29 +2682,24 @@ contract MessageSwitchboardTest is Test, Utils { bytes32 newDigest = createDigest(digestParams); // Create signature digest with both old and new digests with non-watcher key - bytes32 signatureDigest = keccak256( + bytes32 messageHash = keccak256( abi.encodePacked( toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, oldDigest, - newDigest + newDigest, + nonce ) ); - bytes memory signature = createSignature(signatureDigest, nonWatcherKey); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, nonWatcherKey); // Reset transmitter for the function call digestParams.transmitter = toBytes32Format(oldTransmitter); // Should revert because signer is not a watcher - vm.prank(nonWatcher); vm.expectRevert(WatcherNotFound.selector); - messageSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature - ); + _callAssignTransmitter(digestParams, oldTransmitter, newTransmitter, nonce, signatures); } function test_AssignTransmitter_NonceAlreadyUsed_Reverts() public { @@ -2601,53 +2724,49 @@ contract MessageSwitchboardTest is Test, Utils { oldTransmitter ); - // Create signature for new transmitter - bytes memory signature = _createAssignTransmitterSignature( - digestParams, - newTransmitter, - nonce - ); + bytes32 oldDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(newTransmitter); + bytes32 newDigest = createDigest(digestParams); + digestParams.transmitter = toBytes32Format(oldTransmitter); - // First call succeeds - vm.prank(getWatcherAddress()); - messageSwitchboard.assignTransmitter( - digestParams, - oldTransmitter, - newTransmitter, - nonce, - signature + // Create single watcher signature + bytes32 messageHash = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + oldDigest, + newDigest, + nonce + ) ); + bytes[] memory signatures = new bytes[](1); + signatures[0] = createSignature(messageHash, watcherPrivateKey); - // Second call with same nonce should revert - // Need to update oldTransmitter to the new one since we already assigned it - address updatedOldTransmitter = newTransmitter; - address anotherNewTransmitter = address(0x6000); + // First call succeeds + _callAssignTransmitter(digestParams, oldTransmitter, newTransmitter, nonce, signatures); - // Create new digest params with updated old transmitter - DigestParams memory updatedDigestParams = _createAssignTransmitterDigestParams( - payloadId, - address(triggerPlug), - appGatewayId, - payload, - updatedOldTransmitter - ); + // Second call with same nonce should revert (digest already updated, so oldDigest mismatch) + vm.expectRevert(InvalidDigest.selector); + _callAssignTransmitter(digestParams, oldTransmitter, newTransmitter, nonce, signatures); + } - // Create signature for the new assignment - bytes memory signature2 = _createAssignTransmitterSignature( - updatedDigestParams, - anotherNewTransmitter, - nonce - ); + function _callAssignTransmitter( + DigestParams memory digestParams_, + address oldTransmitter_, + address newTransmitter_, + uint256 nonce_, + bytes[] memory signatures_ + ) internal { + MessageSwitchboard.AssignTransmitterParams memory params = MessageSwitchboard + .AssignTransmitterParams({ + digestParams: digestParams_, + oldTransmitter: oldTransmitter_, + newTransmitter: newTransmitter_, + nonce: nonce_, + signatures: signatures_ + }); - vm.prank(getWatcherAddress()); - vm.expectRevert(NonceAlreadyUsed.selector); - messageSwitchboard.assignTransmitter( - updatedDigestParams, - updatedOldTransmitter, - anotherNewTransmitter, - nonce, // Same nonce - should revert - signature2 - ); + messageSwitchboard.assignTransmitter(params); } }