Skip to content

Conversation

@ameeshaagrawal
Copy link
Collaborator

@ameeshaagrawal ameeshaagrawal commented Nov 24, 2025

Summary by CodeRabbit

Release Notes

  • Documentation

    • Comprehensive updates to audit documentation, including expanded setup guides, security model clarifications, and enhanced test coverage documentation.
  • Refactor

    • Improved attestation system with enhanced per-watcher tracking and digest validation.
    • Optimized watcher management through governance-controlled role assignments.
  • Tests

    • Updated test suite to align with architectural improvements and refined verification flows.
  • Style

    • Code formatting and whitespace improvements throughout.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (9)
  • main
  • master
  • dev
  • stage
  • prod
  • staging
  • development
  • production
  • release

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR refactors attestation tracking from global boolean flags to per-watcher cumulative models in EVMxSwitchboard and MessageSwitchboard. It removes payloadIdToDigest storage from Socket.sol, introduces per-chain watcher governance via grantWatcherRole/revokeWatcherRole, and expands audit documentation significantly.

Changes

Cohort / File(s) Summary
Documentation Expansion
auditor-docs/AUDIT_FOCUS_AREAS.md, auditor-docs/AUDIT_PREP_SUMMARY.md, auditor-docs/CONTRACTS_REFERENCE.md, auditor-docs/FAQ.md, auditor-docs/MESSAGE_FLOW.md, auditor-docs/README.md, auditor-docs/SECURITY_MODEL.md, auditor-docs/SETUP_GUIDE.md, auditor-docs/SYSTEM_OVERVIEW.md, auditor-docs/TESTING_COVERAGE.md
Extensive editorial updates, formatting standardization, expanded descriptions of audit focus, testing coverage, system architecture, security model, and setup guidance. No functional code changes.
Socket Digest Storage Removal
contracts/protocol/Socket.sol, contracts/protocol/interfaces/ISocket.sol
Removed public payloadIdToDigest mapping and its assignment in _verify. Removes public payloadIdToDigest(bytes32) view function from interface. Constructor formatting adjusted.
EVMx Switchboard Per-Watcher Attestation
contracts/protocol/switchboard/EVMxSwitchboard.sol
Replaces global isAttested boolean with per-watcher mapping isAttested[address][bytes32]. Adds attestations counter, isValid flag, totalWatchers, and usedNonces tracking. Introduces grantWatcherRole/revokeWatcherRole with access control overrides. setRevertingPayload now requires nonce and signature.
MessageSwitchboard Per-Chain Watcher Model
contracts/protocol/switchboard/MessageSwitchboard.sol
Replaces global isAttested with per-watcher per-chain mapping. Adds totalWatchers per chain, attestations counter, isValid flag, payloadIdToDigest mapping. Introduces grantWatcherRole/revokeWatcherRole with per-chain scope. setRevertingPayload signature expanded with nonce and signature. grantRole/revokeRole overridden to enforce role whitelist.
Switchboard Access Control and Initialization
contracts/protocol/switchboard/SwitchboardBase.sol
Added GOVERNANCE_ROLE import. Moved _initializeOwner(owner_) invocation to earlier in constructor (before chainSlug and socket assignment).
Error Definitions
contracts/utils/common/Errors.sol
Added two new error declarations: InvalidRole() and WatcherFound().
Protocol Contract Formatting
contracts/protocol/SocketConfig.sol, contracts/protocol/SocketUtils.sol, contracts/protocol/base/MessagePlugBase.sol
Minor formatting: spacing in equality checks, whitespace adjustments in comments. No behavioral changes.
EVMx Contract Updates
contracts/evmx/base/AppGatewayBase.sol, contracts/evmx/helpers/ForwarderSolana.sol
Reflowed line continuations in AppGatewayBase. In ForwarderSolana, reformatted signatures and added InvalidSolanaChainSlug revert when chain slug is invalid.
Constants and Miscellaneous
contracts/utils/common/Constants.sol, .prettierignore
Removed trailing blank line in Constants.sol. Added foundry.lock to .prettierignore.
Test Setup and Access Control
test/SetupTest.t.sol
Replaced direct WATCHER_ROLE grants with GOVERNANCE_ROLE and explicit grantWatcherRole per chain. Updated role grant patterns for messageSwitchboard.
Switchboard Test Updates
test/protocol/switchboard/EVMxSwitchboard.t.sol, test/protocol/switchboard/MessageSwitchboard.t.sol
Replaced isAttested(digest) assertions with isValid(digest). Added nonce-based signatures and digest computations for setRevertingPayload calls. Updated role-granting patterns. Reformatted helper functions for readability.
Socket Tests
test/protocol/Socket.t.sol
Removed test_Execute_StoresDigest() test that validated payload digest storage. Reformatted constructor invocations.
Encoding and Protocol Tests
test/encode.t.sol, test/PausableTest.t.sol
Minor formatting and indentation adjustments. No behavioral changes.

Sequence Diagram(s)

sequenceDiagram
    participant Watcher
    participant Switchboard
    participant Storage

    rect rgb(200, 220, 255)
    note over Watcher,Storage: OLD: Global Attestation Boolean
    Watcher->>Switchboard: attest(digest)
    Switchboard->>Storage: isAttested[digest] = true
    Note over Storage: Single boolean, no per-watcher tracking
    Switchboard->>Switchboard: allowPayload checks isAttested[digest]
    end

    rect rgb(220, 200, 255)
    note over Watcher,Storage: NEW: Per-Watcher Cumulative Model
    Watcher->>Switchboard: attest(digest) with signature
    Switchboard->>Storage: isAttested[watcher][digest] = true
    Switchboard->>Storage: attestations[digest]++
    alt attestations[digest] >= totalWatchers
        Switchboard->>Storage: isValid[digest] = true
    end
    Note over Storage: Per-watcher tracking + threshold validation
    Switchboard->>Switchboard: allowPayload checks isValid[digest]
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • EVMxSwitchboard.sol and MessageSwitchboard.sol: Complete refactor of attestation state management. Verify cumulative logic: does attestations[digest]++ and isValid[digest] = true guard correctly prevent double-counting? Check that totalWatchers and isAttested multi-layer mapping prevent replay attacks.
  • Access control override logic in MessageSwitchboard: grantRole and revokeRole now enforce a whitelist. Confirm that this doesn't break existing role management patterns and that WATCHER_ROLE cannot be granted directly anymore.
  • Per-chain watcher governance in MessageSwitchboard: grantWatcherRole and revokeWatcherRole are now chain-specific. Verify nonce validation and signature recovery in setRevertingPayload align with the watcher role check.
  • Socket.sol payloadIdToDigest removal: Confirms MessageSwitchboard now owns digest-to-payload mapping via payloadIdToDigest. Check that processPayload stores this mapping correctly and that removal from Socket doesn't break any digest verification logic.
  • Test alignment: Multiple test files updated to use isValid instead of isAttested and grantWatcherRole instead of direct role grants. Ensure test assertions reflect actual contract behavior and that removed test (test_Execute_StoresDigest) is genuinely obsolete.

Possibly related PRs

  • Fix watcher #214: Adds payloadIdToDigest to MessageSwitchboard and related attestation/deadline behaviors; this PR relocates digest storage from Socket to MessageSwitchboard, directly conflicting with the storage location strategy.
  • Socket test #186: Modifies Socket.sol digest handling and tests; this PR's removal of payloadIdToDigest from Socket overlaps with those changes.
  • Deploy scripts #185: Overlapping switchboard and Socket surface changes around attestation mappings and watcher role management.

Suggested reviewers

  • arthcp

Per-watcher watchers rise, 🔐
Cumulative trust takes flight,
Digests find new homes,
Thresholds crossed just right,
Attestations burn bright.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feat/multi watcher' is vague and generic, using 'Feat/' prefix and minimal descriptive content without conveying the actual scope of changes. Use a clearer title that describes the main change, such as 'Add per-chain watcher attestation model' or 'Implement multi-watcher governance for switchboards'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands and usage tips.


isAttested[digest_] = true;
// Prevent double attestation
if (isAttested[watcher][digest_]) revert AlreadyAttested();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is digest not unique enough to check if it was attested ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here every watcher will attest on same digest, digest doesn't include watcher address

@ameeshaagrawal
Copy link
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
test/PausableTest.t.sol (1)

224-244: These tests don't verify revert behavior.

Lines 224-244 pause the watcher then only assert it's paused—they never call executePayload() or resolvePayload() to test that these functions actually revert. The function names promise to test revert behavior but don't deliver.

Compare with the correct tests at lines 246-255 and 257-273, which actually invoke the functions and expect reverts. Remove these incomplete tests or rewrite them to match the pattern of the working tests below.

Apply this diff to remove the incomplete tests:

-    function test_Watcher_ExecutePayload_WhenPaused_ShouldRevert() public {
-        // Pause the contract
-        vm.prank(owner);
-        watcher.grantRole(PAUSER_ROLE, pauser);
-        vm.prank(pauser);
-        watcher.pause();
-
-        // The executePayload function should revert due to whenNotPaused modifier
-        assertTrue(watcher.paused());
-    }
-
-    function test_Watcher_ResolvePayload_WhenPaused_ShouldRevert() public {
-        // Pause the contract
-        vm.prank(owner);
-        watcher.grantRole(PAUSER_ROLE, pauser);
-        vm.prank(pauser);
-        watcher.pause();
-
-        // The resolvePayload function should revert due to whenNotPaused modifier
-        assertTrue(watcher.paused());
-    }

     function test_Watcher_executePayload_WhenPaused_ShouldRevert() public {
auditor-docs/MESSAGE_FLOW.md (1)

203-277: Attestation and digest storage description is stale vs refactored design

This section still documents the old model where:

  • Socket stores payloadIdToDigest[payloadId] = digest in _verify().
  • Switchboards expose a single isAttested[digest] boolean, and allowPayload simply returns that flag.

Per the PR summary, the implementation now:

  • Removes payloadIdToDigest storage from Socket.sol.
  • Refactors attestation tracking to a per‑watcher cumulative model in EVMxSwitchboard and MessageSwitchboard (likely with thresholds/weights and “valid” status rather than a single global bool).

This should be updated so that:

  • Socket’s role in digest storage is described accurately (or omitted if Socket no longer holds digest state).
  • The attestation flow explains how individual watcher attestations are combined, what constitutes a “valid” payload, and which contract owns the final verdict.
  • The allowPayload behavior matches the new per‑watcher logic instead of returning a legacy isAttested[digest] boolean.

As written, auditors and integrators reading this flow will reason about the wrong trust and replay properties.

Also applies to: 354-417

auditor-docs/SECURITY_MODEL.md (1)

203-263: Security invariants and state lists still reference removed/changed attestation storage

The “Critical Invariants” and “State Modification Points” sections still assume:

  • A write‑once payloadIdToDigest[payloadId] mapping owned by Socket.
  • A global boolean isAttested[digest] that flips from false to true and never back.
  • These are listed as core replay/authorization state (executionStatus, payloadIdToDigest, isAttested, etc.).

Per the PR summary, the implementation now:

  • Removes payloadIdToDigest from Socket.sol.
  • Moves to a per‑watcher cumulative attestation model in the switchboards.

This should be updated so that:

  • Invariants describe whatever replaces payloadIdToDigest (e.g., digest computation rules and any new binding between payload parameters and switchboard state).
  • Attestation invariants describe the per‑watcher aggregation (e.g., thresholds/weights, monotonicity of individual watcher votes) rather than a single global isAttested[digest] flag.
  • The list of “High Impact State Changes” matches actual storage variables in Socket and the switchboards after the refactor.

Leaving these invariants in their pre‑refactor form will cause auditors to reason about guarantees that the code no longer enforces.

Also applies to: 265-283, 297-313, 363-378

auditor-docs/TESTING_COVERAGE.md (1)

50-55: Update Socket.sol coverage section to reflect digest storage moving out of Socket

Socket.sol tests still list payloadIdToDigest storage under “State Management”, but the contract no longer stores digests; that logic now lives in switchboard contracts. This should be updated so auditors and reviewers focus digest-related tests on the correct contract (e.g., MessageSwitchboard.sol) and don’t chase a non-existent mapping in Socket.sol.

Also applies to: 89-101

test/protocol/switchboard/EVMxSwitchboard.t.sol (1)

12-13: Import GOVERNANCE_ROLE or adjust usage to avoid compile error

This test uses GOVERNANCE_ROLE in evmxSwitchboard.grantRole(GOVERNANCE_ROLE, owner); but only imports WATCHER_ROLE from AccessRoles.sol. Unless GOVERNANCE_ROLE is brought into scope elsewhere (it isn’t in this file), this will not compile.

This should either:

  • extend the import to include GOVERNANCE_ROLE, e.g.
    import {GOVERNANCE_ROLE, WATCHER_ROLE} from ".../AccessRoles.sol";
    or
  • switch to whatever role/token the contract actually expects for grantWatcherRole administration.

Without that, these tests won’t run.

Also applies to: 57-61

🧹 Nitpick comments (10)
auditor-docs/FAQ.md (3)

8-45: Consider converting bold subsection headers to markdown headings for better document structure.

Lines like **A1: Switchboards are trusted by Plugs/Apps** use emphasis instead of proper markdown headings (e.g., ### A1: Switchboards are trusted by Plugs/Apps). This pattern repeats throughout the Assumptions and subsequent Q&A sections. While functionally fine, converting bold sections to ### or #### headings would improve markdown compliance and enable better document parsing/navigation.


445-445: Add language identifiers to fenced code blocks.

Lines 445 and 508 contain fenced code blocks without language specifications. Line 445 (transaction ordering example) and line 508 (fee distribution example) should specify a language hint (e.g., ```solidity or ```text) for proper syntax highlighting and markdown compliance.

Also applies to: 508-508


533-545: Consolidate repetitive "only" adverbs in the Q16 answer.

Lines 534–538 use "only" four times in close proximity ("Can only increase," "can only increase," "only the source plug," etc.). This creates a monotonous rhythm. Rewording a few instances (e.g., "Can only increase" → "Cannot decrease") improves readability without changing meaning.

contracts/protocol/SocketUtils.sol (1)

24-27: Digest/return-data behavior unchanged; consider tightening comment wording

The only changes here are to the maxCopyBytes declaration formatting and the _createDigest doc comment. Runtime behavior of digest creation and simulate() is unchanged and still matches the intended pattern (length‑prefixed variable fields + abi.encodePacked).

This comment should explicitly say something like “Uses abi.encodePacked instead of abi.encode for bytes fields, together with length prefixes, to remain collision‑resistant and cross‑chain compatible.” to avoid confusion when auditors scan for collision risks.

Also applies to: 61-62

auditor-docs/MESSAGE_FLOW.md (1)

21-21: Add languages to fenced code blocks for linting and readability

Many code fences use bare triple‑backticks. This should add appropriate languages (for example solidity, text, or bash) to each of these to satisfy MD040 and make rendered highlighting clearer for auditors.

Also applies to: 36-36, 51-51, 159-159, 185-185, 193-193, 199-199, 205-205, 213-213, 238-238, 246-246, 259-259, 293-293, 301-301, 310-310, 643-643, 650-650, 656-656

auditor-docs/SECURITY_MODEL.md (1)

209-213: Fix fenced code languages and stray nonce reference for markdown hygiene

Several fenced code examples in the invariants/attack‑surface/nonce‑management sections use bare ``` without a language, and markdownlint flags an undefined reference for “nonce”.

This should:

  • Add an explicit language to each fence (for example solidity for code‑like invariants or text for logical formulas) to satisfy MD040 and improve readability.
  • Ensure any reference‑style link or footnote label involving nonce (e.g., [nonce]) is either defined at the bottom of the doc or rewritten as inline code nonce to satisfy MD052.

These are low‑risk doc cleanups but will keep the security docs linter‑clean for audit pipelines.

Also applies to: 221-225, 233-237, 245-247, 257-259, 267-271, 279-281, 289-291, 299-302, 310-313, 323-331, 338-343, 351-358, 383-390, 397-402

auditor-docs/SETUP_GUIDE.md (2)

584-596: Add language identifier to code fence.

The fenced code block should specify a language for proper syntax highlighting.

Apply this diff:

-```
+```text
 Socket: 0x... (to be deployed)
 MessageSwitchboard: 0x... (to be deployed)
 FastSwitchboard: 0x... (to be deployed)

---

`617-625`: **Format URLs as proper markdown links.**

Bare URLs should be formatted as markdown links for better readability and consistency.



Apply this diff:

```diff
-- Solidity Docs: https://docs.soliditylang.org/
-- Foundry Book: https://book.getfoundry.sh/
-- Solady Docs: https://github.com/Vectorized/solady
+- [Solidity Docs](https://docs.soliditylang.org/)
+- [Foundry Book](https://book.getfoundry.sh/)
+- [Solady Docs](https://github.com/Vectorized/solady)
 
 **Security Resources**:
 
-- Smart Contract Security Best Practices: https://consensys.github.io/smart-contract-best-practices/
-- DeFi Security Tools: https://github.com/crytic/building-secure-contracts
+- [Smart Contract Security Best Practices](https://consensys.github.io/smart-contract-best-practices/)
+- [DeFi Security Tools](https://github.com/crytic/building-secure-contracts)
auditor-docs/TESTING_COVERAGE.md (1)

666-687: Fix markdown headings to satisfy MD036 and improve structure

Sections like “Invariant 1: Execution Uniqueness”, “Invariant 2: Fee Conservation”, etc., are formatted as bold text instead of markdown headings, which triggers MD036 (no-emphasis-as-heading). These should be changed to proper headings (for example, ### Invariant 1: Execution Uniqueness) so the document structure is clearer and markdown linters pass cleanly.

Also applies to: 702-711, 714-724, 763-776, 778-787

test/protocol/switchboard/EVMxSwitchboard.t.sol (1)

76-95: Helper factories for params are clearer and reduce duplication

The refactored _createExecutionParams, _createExecutionParams (overload), _createTransmissionParams, and _createTransmissionParams (overload), plus _createPayloadAndOverrides, now construct the structs directly in the return statement and centralize default values (e.g., WRITE callType, zeroed prevBatchDigestHash/extraData, default overrides).

This should:

  • reduce repetition across tests,
  • keep callType consistently locked to WRITE in these helpers,
  • make it easier to tweak defaults in a single place.

No functional issues here.

Also applies to: 100-122, 127-135, 166-173

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7075cb3 and dc293f2.

📒 Files selected for processing (29)
  • .prettierignore (1 hunks)
  • auditor-docs/AUDIT_FOCUS_AREAS.md (30 hunks)
  • auditor-docs/AUDIT_PREP_SUMMARY.md (12 hunks)
  • auditor-docs/CONTRACTS_REFERENCE.md (16 hunks)
  • auditor-docs/FAQ.md (49 hunks)
  • auditor-docs/MESSAGE_FLOW.md (28 hunks)
  • auditor-docs/README.md (20 hunks)
  • auditor-docs/SECURITY_MODEL.md (22 hunks)
  • auditor-docs/SETUP_GUIDE.md (33 hunks)
  • auditor-docs/SYSTEM_OVERVIEW.md (10 hunks)
  • auditor-docs/TESTING_COVERAGE.md (54 hunks)
  • contracts/evmx/base/AppGatewayBase.sol (1 hunks)
  • contracts/evmx/helpers/ForwarderSolana.sol (2 hunks)
  • contracts/protocol/Socket.sol (2 hunks)
  • contracts/protocol/SocketConfig.sol (1 hunks)
  • contracts/protocol/SocketUtils.sol (2 hunks)
  • contracts/protocol/base/MessagePlugBase.sol (1 hunks)
  • contracts/protocol/interfaces/ISocket.sol (0 hunks)
  • contracts/protocol/switchboard/EVMxSwitchboard.sol (9 hunks)
  • contracts/protocol/switchboard/MessageSwitchboard.sol (8 hunks)
  • contracts/protocol/switchboard/SwitchboardBase.sol (2 hunks)
  • contracts/utils/common/Constants.sol (0 hunks)
  • contracts/utils/common/Errors.sol (1 hunks)
  • test/PausableTest.t.sol (1 hunks)
  • test/SetupTest.t.sol (1 hunks)
  • test/encode.t.sol (10 hunks)
  • test/protocol/Socket.t.sol (2 hunks)
  • test/protocol/switchboard/EVMxSwitchboard.t.sol (16 hunks)
  • test/protocol/switchboard/MessageSwitchboard.t.sol (18 hunks)
💤 Files with no reviewable changes (2)
  • contracts/utils/common/Constants.sol
  • contracts/protocol/interfaces/ISocket.sol
🧰 Additional context used
🪛 LanguageTool
auditor-docs/FAQ.md

[style] ~536-~536: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...the source plug can increase fees - Can only increase, not decrease - Native fees: A...

(ADVERB_REPETITION_PREMIUM)


[style] ~635-~635: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...ource plug can increase fees 2. Refunds only to specified refundAddress 3. Refunds o...

(ADVERB_REPETITION_PREMIUM)


[style] ~636-~636: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...y to specified refundAddress 3. Refunds only when watcher-approved 4. Refunds only p...

(ADVERB_REPETITION_PREMIUM)


[style] ~637-~637: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...s only when watcher-approved 4. Refunds only possible once 5. Fees in successful exe...

(ADVERB_REPETITION_PREMIUM)

auditor-docs/AUDIT_PREP_SUMMARY.md

[uncategorized] ~124-~124: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...g, not race condition; low probability, low impact - Verdict: Acceptable **9. Cross-C...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[grammar] ~129-~129: Ensure spelling is correct
Context: ...y** - Reason: CEI pattern + unique payloadIds per call - Verdict: Safe by design ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

auditor-docs/SECURITY_MODEL.md

[style] ~104-~104: To form a complete sentence, be sure to include a subject.
Context: ...e (Fully adversarial) Behavior: - May be malicious or buggy - Can attempt ree...

(MISSING_IT_THERE)


[style] ~149-~149: This phrase is redundant (‘OS’ stands for ‘operating system’). Use simply “DOS”.
Context: ...or**: - May underpay fees - May try to DOS system with spam - May attempt double-spending...

(ACRONYM_TAUTOLOGY)

🪛 markdownlint-cli2 (0.18.1)
auditor-docs/FAQ.md

14-14: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


20-20: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


26-26: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


33-33: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


40-40: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


362-362: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


445-445: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


508-508: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


997-997: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

auditor-docs/MESSAGE_FLOW.md

21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


36-36: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


159-159: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


185-185: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


193-193: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


199-199: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


205-205: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


213-213: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


238-238: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


246-246: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


259-259: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


293-293: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


301-301: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


310-310: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


643-643: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


650-650: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


656-656: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

auditor-docs/CONTRACTS_REFERENCE.md

335-335: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

auditor-docs/TESTING_COVERAGE.md

678-678: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


690-690: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


714-714: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


763-763: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


778-778: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

auditor-docs/SECURITY_MODEL.md

209-209: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


221-221: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


233-233: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


399-399: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"

(MD052, reference-links-images)


400-400: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"

(MD052, reference-links-images)

auditor-docs/SETUP_GUIDE.md

584-584: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


617-617: Bare URL used

(MD034, no-bare-urls)


618-618: Bare URL used

(MD034, no-bare-urls)


619-619: Bare URL used

(MD034, no-bare-urls)


623-623: Bare URL used

(MD034, no-bare-urls)


624-624: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (23)
.prettierignore (1)

39-40: Entries appropriately ignore generated and documentation artifacts.

Adding foundry.lock (generated by Foundry's package manager) and internal-audit/ (audit documentation directory) to Prettier's ignore list makes sense for this project setup. Both entries follow the existing pattern and won't conflict with other ignores.

contracts/evmx/base/AppGatewayBase.sol (1)

115-116: Line reflow looks good. This formatting change improves readability without altering behavior. The method chaining across lines follows standard Solidity style patterns.

test/PausableTest.t.sol (2)

9-9: Import formatting corrected.

Minor indentation adjustment for consistency.


246-273: Correct test pattern.

These tests properly call the functions and expect reverts. The ones immediately above (lines 224-244) should be removed in favor of these.

auditor-docs/FAQ.md (1)

1-1235: FAQ content is comprehensive and well-structured; no functional issues detected.

The documentation effectively covers system assumptions, architecture, security, operations, fees, and edge cases. Changes align with the PR's focus on multi-watcher and per-chain governance improvements. Note: Q30 (deadline enforcement) and related sections could reference the new grantWatcherRole/revokeWatcherRole governance functions if they introduce changes to watcher trust models, but this appears to be intentionally deferred to contract-specific documentation.

contracts/protocol/base/MessagePlugBase.sol (1)

24-33: No behavioral change in constructor

Change here is formatting-only (blank line removal). Validation, _setSocket, switchboardId, and switchboard wiring plus connect call all behave exactly as before.

contracts/protocol/SocketConfig.sol (1)

141-151: Spacing-only change in getPlugConfig

This should keep the existing behavior intact (switchboardId == 0 continues to short‑circuit to (0, "")) and just improves readability of the equality check.

contracts/utils/common/Errors.sol (1)

163-167: LGTM!

The new error declarations follow consistent patterns and align with the governance-driven access model introduced in this PR.

contracts/evmx/helpers/ForwarderSolana.sol (2)

32-39: LGTM! Correct validation added.

The else clause now properly reverts with InvalidSolanaChainSlug() when the chain slug is neither Solana mainnet nor devnet. This is a correctness improvement over the previous implementation.


66-66: LGTM! Formatting change only.

Single-line function signature is more concise without changing behavior.

test/encode.t.sol (1)

18-18: LGTM! Formatting changes only.

These are pure formatting adjustments with no functional impact.

auditor-docs/README.md (1)

1-597: LGTM! Documentation improvements.

The expanded documentation structure with additional sections and formatting improvements enhances audit readiness and reviewer experience.

auditor-docs/AUDIT_FOCUS_AREAS.md (1)

1-728: LGTM! Comprehensive audit focus documentation.

The expanded documentation with explicit file references, code examples, and priority classifications provides clear guidance for auditors.

contracts/protocol/switchboard/SwitchboardBase.sol (2)

8-8: LGTM! Added GOVERNANCE_ROLE import.

This import aligns with the broader governance role management introduced in this PR.


40-49: LGTM! Improved constructor initialization order.

Moving _initializeOwner(owner_) before setting chainSlug and socket__ follows best practice by initializing access control first. This ensures ownership is established before any other state initialization.

test/protocol/Socket.t.sol (2)

25-30: LGTM! Formatting change only.

Multi-line constructor parameters improve readability without changing behavior.


283-288: LGTM! Formatting change only.

Multi-line constructor parameters improve readability without changing behavior.

test/SetupTest.t.sol (1)

233-242: Watcher governance wiring for switchboards looks correct

This setup should give socketOwner governance/rescue on both switchboards and correctly assign the watcher EOA as:

  • global watcher on EVMxSwitchboard, and
  • per-chain watcher on MessageSwitchboard for both arbChainSlug and optChainSlug.

This matches the per-switchboard/per-chain watcher model; no issues here.

contracts/protocol/Socket.sol (1)

29-35: Constructor and fallback encoding remain correct

The constructor still just forwards its parameters to SocketUtils with no behavioral change, and the fallback’s manual ABI encoding of bytes32 as bytes (offset=0x20, length=0x20, followed by payloadId) is intact. This should keep all existing call patterns working as before.

Also applies to: 242-255

test/protocol/switchboard/EVMxSwitchboard.t.sol (4)

422-433: isValid assertion matches the new attestation API

test_Attest_Success now asserts evmxSwitchboard.isValid(digest) after a successful attest. This should track the new validation API (replacing isAttested) and ensures tests are checking the right public surface for watcher attestations.


594-608: Nonce + signature tests for setRevertingPayload match the described scheme

The setRevertingPayload tests now:

  • include a nonce,
  • hash address(evmxSwitchboard), CHAIN_SLUG, payloadId, isReverting, and nonce,
  • sign with the watcher key for the success path,
  • use a mismatched signer for the failure path.

This should exercise:

  • watcher authorization,
  • correct digest domain separation (contract address + chain slug),
  • nonce replay protection for this selector/flow.

This must stay in sync with the on-chain setRevertingPayload implementation’s digest construction and _validateAndUseNonce usage; as long as those match, these tests look correct.

Also applies to: 612-638


644-657: Default deadline event and state assertion cover the new API

test_SetDefaultDeadline_Success now:

  • expects DefaultDeadlineSet(newDeadline) and
  • asserts evmxSwitchboard.defaultDeadline() == newDeadline.

This should fully cover the new event and ensure the stored default deadline actually updates, not just the emission.


697-754: Custom vs zero-deadline processPayload behavior is exercised correctly

The ProcessPayload tests now distinguish:

  • non-zero overrides: only check that a non-zero payloadId is produced and the counter increments, and
  • zero overrides: expect a PayloadRequested event whose overrides encode block.timestamp + defaultDeadline().

This should validate both “explicit deadline” and “use default” branches in processPayload without overfitting to internal implementation details.

Comment on lines 36 to 41
**2. Replay Protection**

- **Status**: ✅ Multi-layer protection in place
- **Mechanisms**: executionStatus, isAttested, nonce system
- **Result**: No double-execution or replay possible

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align replay protection description with isValid API

Replay protection is described as using executionStatus, isAttested, nonce system, but the switchboard API now exposes isValid(...) instead of isAttested(...). This should use the current name (isValid) so auditors don’t chase a non-existent function and so the documented mechanisms match the deployed interface.

Also applies to: 176-187

🤖 Prompt for AI Agents
In auditor-docs/AUDIT_PREP_SUMMARY.md around lines 36-41 (and also update the
similar block at lines 176-187), the document references a non-existent function
name isAttested; change the wording to reference the current switchboard API
function isValid(...) and adjust any explanatory text to reflect the isValid
naming (e.g., replace “isAttested” with “isValid” and ensure examples/mentions
match the deployed interface) so the replay protection mechanisms accurately
match the live API.

Comment on lines 152 to +199
## Access Control Matrix

| Function | Contract | Roles Required | Restriction |
|----------|----------|----------------|-------------|
| `execute()` | Socket | None | Not paused, valid params |
| `sendPayload()` | Socket | None | Not paused, connected plug |
| `connect()` | Socket | None (msg.sender = plug) | Valid switchboard |
| `disconnect()` | Socket | None (msg.sender = plug) | Currently connected |
| `registerSwitchboard()` | Socket | None (msg.sender = switchboard) | Not already registered |
| `disableSwitchboard()` | SocketConfig | SWITCHBOARD_DISABLER_ROLE | - |
| `enableSwitchboard()` | SocketConfig | GOVERNANCE_ROLE | - |
| `setNetworkFeeCollector()` | SocketConfig | GOVERNANCE_ROLE | - |
| `setGasLimitBuffer()` | SocketConfig | GOVERNANCE_ROLE | >= 100 |
| `setMaxCopyBytes()` | SocketConfig | GOVERNANCE_ROLE | - |
| `pause()` | SocketUtils | PAUSER_ROLE | - |
| `unpause()` | SocketUtils | UNPAUSER_ROLE | - |
| `rescueFunds()` | SocketUtils/Switchboards | RESCUE_ROLE | - |
| `attest()` | Switchboards | WATCHER_ROLE | Valid signature |
| `markRefundEligible()` | MessageSwitchboard | WATCHER_ROLE | Valid signature + nonce |
| `refund()` | MessageSwitchboard | None | Must be eligible |
| `setMinMsgValueFees()` | MessageSwitchboard | FEE_UPDATER_ROLE | Valid signature + nonce |
| `setEvmxConfig()` | FastSwitchboard | onlyOwner | - |
| `setRevertingPayload()` | Switchboards | onlyOwner | - |
| Function | Contract | Roles Required | Restriction |
| -------------------------- | ------------------------ | ------------------------------- | -------------------------- |
| `execute()` | Socket | None | Not paused, valid params |
| `sendPayload()` | Socket | None | Not paused, connected plug |
| `connect()` | Socket | None (msg.sender = plug) | Valid switchboard |
| `disconnect()` | Socket | None (msg.sender = plug) | Currently connected |
| `registerSwitchboard()` | Socket | None (msg.sender = switchboard) | Not already registered |
| `disableSwitchboard()` | SocketConfig | SWITCHBOARD_DISABLER_ROLE | - |
| `enableSwitchboard()` | SocketConfig | GOVERNANCE_ROLE | - |
| `setNetworkFeeCollector()` | SocketConfig | GOVERNANCE_ROLE | - |
| `setGasLimitBuffer()` | SocketConfig | GOVERNANCE_ROLE | >= 100 |
| `setMaxCopyBytes()` | SocketConfig | GOVERNANCE_ROLE | - |
| `pause()` | SocketUtils | PAUSER_ROLE | - |
| `unpause()` | SocketUtils | UNPAUSER_ROLE | - |
| `rescueFunds()` | SocketUtils/Switchboards | RESCUE_ROLE | - |
| `attest()` | Switchboards | WATCHER_ROLE | Valid signature |
| `markRefundEligible()` | MessageSwitchboard | WATCHER_ROLE | Valid signature + nonce |
| `refund()` | MessageSwitchboard | None | Must be eligible |
| `setMinMsgValueFees()` | MessageSwitchboard | FEE_UPDATER_ROLE | Valid signature + nonce |
| `setEvmxConfig()` | FastSwitchboard | onlyOwner | - |
| `setRevertingPayload()` | Switchboards | onlyOwner | - |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Access control matrix misstates who can call unpause()

The table lists:

  • Contract: SocketUtils
  • Function: unpause()
  • Roles Required: UNPAUSER_ROLE

In the current code, unpause() is declared as:

function unpause() external onlyRole(GOVERNANCE_ROLE) {
    _unpause();
}

This should be updated so the matrix reflects GOVERNANCE_ROLE as the required role, or the implementation changed if the intended role really is UNPAUSER_ROLE. As written, this is a security‑relevant discrepancy for auditors.

🤖 Prompt for AI Agents
In auditor-docs/SECURITY_MODEL.md around lines 177 to 199, the access control
matrix incorrectly states that SocketUtils.unpause() requires UNPAUSER_ROLE
whereas the implementation uses onlyRole(GOVERNANCE_ROLE); update the matrix to
list GOVERNANCE_ROLE for unpause(), or alternatively change the contract
implementation to onlyRole(UNPAUSER_ROLE) if the intended policy is to allow the
UNPAUSER_ROLE to unpause; ensure the doc and code match and note which option
was chosen in the commit message.

Comment on lines 210 to 229
function setRevertingPayload(
bytes32 payloadId_,
bool isReverting_,
uint256 nonce_,
bytes calldata signature_
) external {
bytes32 digest = keccak256(
abi.encodePacked(
toBytes32Format(address(this)),
chainSlug,
payloadId_,
isReverting_,
nonce_
)
);

address watcher = _recoverSigner(digest, signature_);
if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound();
usedNonces[watcher][nonce_] = true;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Block signature replays before toggling revertingPayloadIds.

setRevertingPayload flips the flag using a watcher-signed message but never checks usedNonces[watcher][nonce_] before writing it. Anyone can replay an old signature (e.g., the last isReverting = true message) indefinitely to force the payload back into the reverting state, even after the watcher has cleared it with a new nonce. This should gate the call with a NonceAlreadyUsed check (and revert) before setting the flag, mirroring the _validateAndUseNonce guard in MessageSwitchboard. Add the guard so previously consumed signatures cannot be replayed to overwrite the latest watcher intent.

🤖 Prompt for AI Agents
In contracts/protocol/switchboard/EVMxSwitchboard.sol around lines 210 to 229,
the function setRevertingPayload computes a watcher-signed digest and sets
usedNonces[watcher][nonce_] = true without first checking if that nonce was
already consumed, allowing signature replay to flip reverting flags; add a
nonce-usage guard like _validateAndUseNonce: first check
usedNonces[watcher][nonce_] and revert with NonceAlreadyUsed if true, then
proceed to mark the nonce used only after the check (or call the existing
_validateAndUseNonce helper if available) before toggling revertingPayloadIds so
previously consumed signatures cannot be replayed to overwrite current watcher
intent.

Comment on lines +257 to +295
/**
* @notice adds a watcher
* @param watcher_ watcher address
*/
function grantWatcherRole(address watcher_) external onlyRole(GOVERNANCE_ROLE) {
if (_hasRole(WATCHER_ROLE, watcher_)) revert WatcherFound();
_grantRole(WATCHER_ROLE, watcher_);

++totalWatchers;
}

/**
* @notice removes a watcher
* @param watcher_ watcher address
*/
function revokeWatcherRole(address watcher_) external onlyRole(GOVERNANCE_ROLE) {
if (!_hasRole(WATCHER_ROLE, watcher_)) revert WatcherNotFound();
_revokeRole(WATCHER_ROLE, watcher_);

--totalWatchers;
}

/**
* @dev Overriding this function from AccessControl to make sure owner can't grant Watcher Role directly, and should
* only use grantWatcherRole function instead. This is to make sure watcher count remains correct
*/
function grantRole(bytes32 role_, address grantee_) external override onlyOwner {
if (role_ == WATCHER_ROLE) revert InvalidRole();
_grantRole(role_, grantee_);
}

/**
* @dev Overriding this function from AccessControl to make sure owner can't revoke Watcher Role directly, and should
* only use revokeWatcherRole function instead. This is to make sure watcher count remains correct
*/
function revokeRole(bytes32 role_, address grantee_) external override onlyOwner {
if (role_ == WATCHER_ROLE) revert InvalidRole();
_revokeRole(role_, grantee_);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep totalWatchers in sync when watchers renounce.

You increment totalWatchers on grant and decrement on revoke, but AccessControl still lets a watcher call renounceRole(WATCHER_ROLE, watcher_). That path skips your bookkeeping, so totalWatchers stays higher than the number of active watchers. After one renounce, new digests will never reach the quorum (attestations[digest_] >= totalWatchers) and isValid will remain false. This should either override renounceRole to block watcher self-renounce or adjust totalWatchers when it happens. Without it, the multi-watcher attestation flow bricks itself after a single renounce.

🤖 Prompt for AI Agents
In contracts/protocol/switchboard/EVMxSwitchboard.sol around lines 257-295,
renouncing the WATCHER_ROLE via AccessControl.renounceRole bypasses your
grantWatcherRole/revokeWatcherRole bookkeeping and leaves totalWatchers stale;
either override renounceRole to prevent self-renounce for WATCHER_ROLE or
override it to decrement totalWatchers when a watcher renounces. Implement an
override of renounceRole(bytes32 role_, address account_) that, when role_ ==
WATCHER_ROLE and account_ currently has the role, decrements totalWatchers (or
reverts to force using grantWatcherRole/revokeWatcherRole), then calls the
underlying _revokeRole/parent implementation; otherwise delegate to the parent
behavior. Ensure the function preserves the original access semantics (only the
role holder can renounce their own role) and keeps totalWatchers consistent.

Comment on lines +704 to +748
function grantWatcherRole(
uint32 siblingChainSlug_,
address watcher_
) external onlyRole(GOVERNANCE_ROLE) {
bytes32 role = keccak256(abi.encode(WATCHER_ROLE, siblingChainSlug_));
if (_hasRole(role, watcher_)) revert WatcherFound();
_grantRole(role, watcher_);

++totalWatchers[siblingChainSlug_];
}

/**
* @notice removes a watcher
* @param watcher_ watcher address
*/
function revokeWatcherRole(
uint32 siblingChainSlug_,
address watcher_
) external onlyRole(GOVERNANCE_ROLE) {
bytes32 role = keccak256(abi.encode(WATCHER_ROLE, siblingChainSlug_));
if (!_hasRole(role, watcher_)) revert WatcherNotFound();
_revokeRole(role, watcher_);

--totalWatchers[siblingChainSlug_];
}

/**
* @dev Overriding this function from AccessControl to make sure owner can't grant Watcher Role directly, and should
* only use grantWatcherRole function instead. This is to make sure watcher count remains correct
*/
function grantRole(bytes32 role_, address grantee_) external override onlyOwner {
if (role_ != GOVERNANCE_ROLE && role_ != RESCUE_ROLE && role_ != FEE_UPDATER_ROLE)
revert InvalidRole();
_grantRole(role_, grantee_);
}

/**
* @dev Overriding this function from AccessControl to make sure owner can't revoke Watcher Role directly, and should
* only use revokeWatcherRole function instead. This is to make sure watcher count remains correct
*/
function revokeRole(bytes32 role_, address grantee_) external override onlyOwner {
if (role_ != GOVERNANCE_ROLE && role_ != RESCUE_ROLE && role_ != FEE_UPDATER_ROLE)
revert InvalidRole();
_revokeRole(role_, grantee_);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Account for watcher self-renounce to avoid permanent quorum inflation.

Watchers can still call renounceRole(keccak256(abi.encode(WATCHER_ROLE, siblingChainSlug_)), watcher_) via AccessControl. That path bypasses your revokeWatcherRole bookkeeping, leaving totalWatchers[siblingChainSlug_] unchanged even though the watcher is gone. From then on, attest() never sees attestations[digest_) >= totalWatchers[siblingChainSlug_], so isValid stays false and payloads stop executing. This should either override renounceRole (and adjust the counter) or prevent watchers from renouncing outright; otherwise a single renounce bricks the quorum logic you just introduced.

🤖 Prompt for AI Agents
In contracts/protocol/switchboard/MessageSwitchboard.sol around lines 704-748,
renouncing a watcher role via AccessControl.renounceRole bypasses
revokeWatcherRole and leaves totalWatchers[] stale; add tracking and an override
to handle this: when granting/revoking watcher roles set a mapping(bytes32 =>
uint32) watcherRoleToSlug and/or mapping(bytes32 => bool) isWatcherRole, then
override renounceRole(bytes32 role_, address account_) to detect watcher roles
(via isWatcherRole or watcherRoleToSlug) and either call revokeWatcherRole(slug,
account_) so the counter is decremented, or revert to forbid renounce for
watcher roles; update grantWatcherRole and revokeWatcherRole to populate/clear
the mappings accordingly.

@ameeshaagrawal ameeshaagrawal merged commit a7911af into phase-1 Nov 24, 2025
1 check was pending
@ameeshaagrawal ameeshaagrawal deleted the feat/multi-watcher branch November 24, 2025 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants