From 8479c0ccd44f0355c10a6eac70799ab388c6ef52 Mon Sep 17 00:00:00 2001 From: arthcp Date: Thu, 15 Jan 2026 18:24:49 +0400 Subject: [PATCH 1/2] feat: add missing markIsValid --- contracts/protocol/switchboard/MessageSwitchboard.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/protocol/switchboard/MessageSwitchboard.sol b/contracts/protocol/switchboard/MessageSwitchboard.sol index 930a0345..f94cdaaf 100644 --- a/contracts/protocol/switchboard/MessageSwitchboard.sol +++ b/contracts/protocol/switchboard/MessageSwitchboard.sol @@ -752,6 +752,10 @@ contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { emit TransmitterAssigned(dp.payloadId, params_.newTransmitter); } + function markIsValid(bytes32 payloadId_, bytes32 digest_) external { + if (attestations[payloadId_][digest_] >= totalWatchers) isValid[payloadId_][digest_] = true; + } + /** * @notice Sets the transmitter address for payload execution * @param transmitter_ The new transmitter address From de757054d8f9250ff222cdf892467634fe3a4b1a Mon Sep 17 00:00:00 2001 From: arthcp Date: Thu, 15 Jan 2026 18:30:49 +0400 Subject: [PATCH 2/2] feat: tests for markIsValid --- .../switchboard/EVMxSwitchboard.t.sol | 74 ++++++++++++++ .../switchboard/MessageSwitchboard.t.sol | 99 +++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/test/protocol/switchboard/EVMxSwitchboard.t.sol b/test/protocol/switchboard/EVMxSwitchboard.t.sol index ce22f371..726fdea6 100644 --- a/test/protocol/switchboard/EVMxSwitchboard.t.sol +++ b/test/protocol/switchboard/EVMxSwitchboard.t.sol @@ -1162,6 +1162,80 @@ contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { evmxSwitchboard.revokeRole(WATCHER_ROLE, grantee); } + // ============================================ + // TESTS - markIsValid Recovery Function + // ============================================ + + function test_markIsValid_AfterWatcherRevoked() public { + // Setup: Add 2 more watchers (total 3) + addWatchers(); + assertEq(evmxSwitchboard.totalWatchers(), 3); + + bytes32 digest = keccak256(abi.encode("test payload")); + bytes32 payloadId = bytes32(uint256(0x1234)); + + // Get 2 watchers to attest (not enough for threshold of 3) + bytes32 signatureDigest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId, + digest + ) + ); + + bytes memory signature1 = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(payloadId, digest, signature1); + + bytes memory signature2 = createSignature(signatureDigest, watcher2PrivateKey); + vm.prank(getWatcher2Address()); + evmxSwitchboard.attest(payloadId, digest, signature2); + + // Verify: 2 attestations, isValid still false (need 3) + assertEq(evmxSwitchboard.attestations(payloadId, digest), 2); + assertFalse(evmxSwitchboard.isValid(payloadId, digest)); + + // Revoke watcher3 (totalWatchers becomes 2) + vm.prank(owner); + evmxSwitchboard.revokeWatcherRole(getWatcher3Address()); + assertEq(evmxSwitchboard.totalWatchers(), 2); + + // isValid is still false even though attestations >= totalWatchers + assertFalse(evmxSwitchboard.isValid(payloadId, digest)); + + // Call markIsValid to fix the state + evmxSwitchboard.markIsValid(payloadId, digest); + + // Now isValid should be true + assertTrue(evmxSwitchboard.isValid(payloadId, digest)); + // Attestations count unchanged + assertEq(evmxSwitchboard.attestations(payloadId, digest), 2); + } + + function test_markIsValid_NotEnoughAttestations_NoOp() public { + // Setup: 3 watchers + addWatchers(); + assertEq(evmxSwitchboard.totalWatchers(), 3); + + bytes32 digest = keccak256(abi.encode("test payload")); + bytes32 payloadId = bytes32(uint256(0x1234)); + + // Only 1 watcher attests + bytes memory signature = _createAttestSignature(payloadId, digest); + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(payloadId, digest, signature); + + assertEq(evmxSwitchboard.attestations(payloadId, digest), 1); + assertFalse(evmxSwitchboard.isValid(payloadId, digest)); + + // Call markIsValid - should be no-op since attestations < totalWatchers + evmxSwitchboard.markIsValid(payloadId, digest); + + // isValid should still be false + assertFalse(evmxSwitchboard.isValid(payloadId, digest)); + } + // ============================================ // MISSING TESTS - SetRevertingPayload Edge Cases // ============================================ diff --git a/test/protocol/switchboard/MessageSwitchboard.t.sol b/test/protocol/switchboard/MessageSwitchboard.t.sol index e1674054..d9e6c3bb 100644 --- a/test/protocol/switchboard/MessageSwitchboard.t.sol +++ b/test/protocol/switchboard/MessageSwitchboard.t.sol @@ -986,6 +986,105 @@ contract MessageSwitchboardTest is Test, Utils { assertEq(messageSwitchboard.attestations(payloadId, digest), 1); } + // ============================================ + // TESTS - markIsValid Recovery Function + // ============================================ + + function test_markIsValid_AfterWatcherRevoked() public { + _setupSiblingConfig(); + + // Setup: Add 2 more watchers (total 3) + uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 watcher3Key = 0x3333333333333333333333333333333333333333333333333333333333333333; + address watcher2 = vm.addr(watcher2Key); + address watcher3 = vm.addr(watcher3Key); + + vm.startPrank(owner); + messageSwitchboard.grantWatcherRole(watcher2); + messageSwitchboard.grantWatcherRole(watcher3); + vm.stopPrank(); + assertEq(messageSwitchboard.totalWatchers(), 3); + + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, abi.encode("test")); + bytes32 digest = calculateDigest(digestParams); + + // Get 2 watchers to attest + bytes32 signatureDigest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + digest + ) + ); + + bytes memory signature1 = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(payloadId, digest, signature1); + + bytes memory signature2 = createSignature(signatureDigest, watcher2Key); + vm.prank(watcher2); + messageSwitchboard.attest(payloadId, digest, signature2); + + // Verify: 2 attestations, isValid still false + assertEq(messageSwitchboard.attestations(payloadId, digest), 2); + assertFalse(messageSwitchboard.isValid(payloadId, digest)); + + // Revoke watcher3 (totalWatchers becomes 2) + vm.prank(owner); + messageSwitchboard.revokeWatcherRole(watcher3); + assertEq(messageSwitchboard.totalWatchers(), 2); + + // isValid still false even though attestations >= totalWatchers + assertFalse(messageSwitchboard.isValid(payloadId, digest)); + + // Call markIsValid to fix the state + messageSwitchboard.markIsValid(payloadId, digest); + + // Now isValid should be true + assertTrue(messageSwitchboard.isValid(payloadId, digest)); + assertEq(messageSwitchboard.attestations(payloadId, digest), 2); + } + + function test_markIsValid_NotEnoughAttestations_NoOp() public { + _setupSiblingConfig(); + + // Setup: 2 watchers total + uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; + address watcher2 = vm.addr(watcher2Key); + + vm.prank(owner); + messageSwitchboard.grantWatcherRole(watcher2); + assertEq(messageSwitchboard.totalWatchers(), 2); + + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, abi.encode("test")); + bytes32 digest = calculateDigest(digestParams); + + // Only 1 watcher attests + bytes32 signatureDigest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + digest + ) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(payloadId, digest, signature); + + assertEq(messageSwitchboard.attestations(payloadId, digest), 1); + assertFalse(messageSwitchboard.isValid(payloadId, digest)); + + // Call markIsValid - should be no-op + messageSwitchboard.markIsValid(payloadId, digest); + + // isValid should still be false + assertFalse(messageSwitchboard.isValid(payloadId, digest)); + } + // ============================================ // IMPORTANT TESTS - GROUP 5: Sponsor Approvals // ============================================