Skip to content

Conversation

@ameeshaagrawal
Copy link
Collaborator

@ameeshaagrawal ameeshaagrawal commented Oct 15, 2025

Summary by CodeRabbit

  • New Features

    • Payload-centric flow: submit/add, execute, resolve, cancel, increase-fees and query by payloadId; new watcher APIs and expanded lifecycle events.
    • New override-params builder for fluent payload overrides; async promises and deploys now use payloadId.
    • Message resolution workflow for sponsor-paid executions.
  • Refactor

    • Replaced queue/batch model with single-payload watcher-driven lifecycle; credits/fees now payloadId-based and precompiles updated.
    • Switchboard/socket IDs narrowed to 32-bit and payload ID verification added.
  • Tests

    • Updated and expanded end-to-end tests (counter app, switchboard/message, payload ID verification).
  • Documentation

    • Public API and event docs updated to reflect payload-based signatures and events.

@coderabbitai
Copy link

coderabbitai bot commented Oct 15, 2025

Walkthrough

Migrates the codebase from a queue/request-centric model to a payload-first architecture: replaces numeric request counters and QueueParams with RawPayload and bytes32 payloadId, adds OverrideParamsLib, restructures Watcher/precompile/fees/async flows to addPayloadData → executePayload → resolvePayload/markRevert, and removes legacy RequestHandler surfaces and related APIs.

Changes

Cohort / File(s) Summary
Core types & utils
\contracts/utils/common/Structs.sol`, `contracts/utils/OverrideParamsLib.sol`, `contracts/utils/common/Errors.sol`, `contracts/utils/common/IdUtils.sol``
Introduce RawPayload, reshape Payload/OverrideParams (consumeFrom, switchboardType, maxFees), add OverrideParamsLib fluent setters, replace RequestCountMismatch → PayloadCountMismatch, add create/decode payloadId APIs.
Watcher API & impl
\contracts/evmx/interfaces/IWatcher.sol`, `contracts/evmx/watcher/Watcher.sol`, `contracts/evmx/watcher/Configurations.sol``
Replace queue/batch model with payload lifecycle APIs (addPayloadData,executePayload,resolvePayload,markRevert); add events/accessors (currentPayloadId, nextPayloadCount, latestAsyncPromise, transmitter, triggerFees); validate config updates via signatures.
AppGateway & interfaces
\contracts/evmx/base/AppGatewayBase.sol`, `contracts/evmx/interfaces/IAppGateway.sol``
Add OverrideParams state + use OverrideParamsLib; add currentPayloadId and forwarderAddresses mapping; remove sbType and legacy deploy helpers; add getOverrideParams/applyOverride and adjust IAppGateway returns.
Precompiles
\contracts/evmx/watcher/precompiles/*``
Move handlePayload/resolvePayload to RawPayload/Payload signatures, remove WatcherBase inheritance, gate with onlyWatcher, compute fees/deadlines from RawPayload, add rescueFunds.
Fees & Credits
\contracts/evmx/interfaces/IFeesManager.sol`, `contracts/evmx/fees/FeesManager.sol`, `contracts/evmx/fees/Credit.sol``
Change block/unblock/assign APIs and events to use bytes32 payloadId; integrate OverrideParamsLib and watcher-based consumeFrom/maxFees resolution; adjust deposit signature to accept encoded payload.
Helpers & deployers
\contracts/evmx/helpers/Forwarder.sol`, `contracts/evmx/helpers/AddressResolver*.sol`, `contracts/evmx/helpers/AsyncPromise.sol`, `contracts/evmx/helpers/AsyncDeployer.sol``
Forwarder now builds RawPayload and calls watcher.addPayloadData; AddressResolver drops deployForwarder/defaultAuctionManager and centralizes isWatcher(); AsyncPromise/AsyncDeployer switch from uint40 counters → bytes32 payloadId.
RequestHandler removal / deprecations
\contracts/evmx/watcher/RequestHandler.sol`, `deprecated/RequestHandler.sol`, `deprecated/IRequestHandler.sol``
Remove legacy RequestHandler implementation and many request/batch APIs; deprecated artifacts adjusted or left as stubs.
Interfaces cleanup
\contracts/evmx/interfaces/IPrecompile.sol`, `contracts/evmx/interfaces/IAddressResolver.sol`, `contracts/evmx/interfaces/IPromise.sol`, `contracts/evmx/interfaces/IReceiver.sol``
IPrecompile now uses RawPayload/Payload and payloadId in handle/resolve; removed validateAndGetPrecompileData; IAddressResolver drops deployForwarder/defaultAuctionManager getters/setters; IPromise.requestCount() → payloadId(); IReceiver removed.
Protocol & switchboards
\contracts/protocol/`, `contracts/protocol/switchboard/`, `contracts/protocol/Socket*.sol`, `contracts/protocol/SocketConfig.sol``
Switchboard IDs narrowed uint64→uint32; move plug config to plugSwitchboardIds; ISocket/ISwitchboard expanded (sendPayload, increaseFeesForPayload, updatePlugConfig, getPlugConfig); MessageSwitchboard refactor adds native/sponsored fees, refunds, sponsor approvals and payloadFees storage.
Watcher execute/resolution flow
\contracts/evmx/watcher/`, `contracts/evmx/watcher/precompiles/`, `contracts/evmx/fees/*``
Implement single-payload lifecycle: addPayloadData → executePayload → resolvePayload/markRevert; block/unblock credits by payloadId; add lifecycle events/errors (PayloadSubmitted/Resolved/Cancelled/Settled, PromiseResolved/NotResolved, MarkedRevert).
Tests & apps
\test/`, `test/apps/counter/`, `test/mocks/`, `test/switchboard/``
Tests updated to payload flows; add Counter app and CounterAppGateway; remove MockWatcherPrecompile; add MockPlug and extensive MessageSwitchboard tests; update scripts/tests to payloadId-based interactions.
Build & scripts
\hardhat-scripts/`, `src/`, `package.json`, `.env.sample``
Deployment scripts repointed to Watcher/configs, WatcherMultiCallParams introduced, signing flow changed to produce WatcherMultiCallParams; env key renamed SOCKET_SIGNER_KEY → SOCKET_PRIVATE_KEY; enums/events/types adjusted; package version bumped.

Sequence Diagram(s)

%% payload submission & async deployment (high level)
sequenceDiagram
  autonumber
  participant AppGW as AppGateway
  participant Watcher as Watcher
  participant Pre as Precompile
  participant Fees as FeesManager
  participant Prom as AsyncPromise

  AppGW->>Watcher: addPayloadData(RawPayload, appGateway)
  Watcher->>Watcher: compute payloadId, store RawPayload
  Watcher->>Pre: handlePayload(RawPayload, appGateway, payloadId)
  Pre-->>Watcher: (fees, deadline, precompileData)
  Watcher->>Fees: blockCredits(payloadId, consumeFrom, fees)
  Fees-->>Watcher: CreditsBlocked(payloadId,...)
  Watcher-->>AppGW: PayloadSubmitted(payloadId)
  alt needs async promise
    Watcher->>Prom: deployAsyncPromiseContract(invoker, payloadId)
    Prom-->>Watcher: latestAsyncPromise
  end
Loading
%% execute -> resolve / cancel flows (high level)
sequenceDiagram
  autonumber
  participant Watcher as Watcher
  participant Prom as AsyncPromise
  participant Pre as Precompile
  participant Fees as FeesManager
  participant AppGW as AppGateway

  Watcher->>Watcher: executePayload()
  Watcher->>Prom: check/await latestAsyncPromise
  alt promise resolved
    Prom-->>Watcher: PromiseResolved
    Watcher->>Pre: resolvePayload(Payload)
    Pre-->>Watcher: callback result
    Watcher->>Fees: unblockAndAssignCredits(payloadId, transmitter)
    Fees-->>Watcher: CreditsUnblockedAndAssigned
    Watcher-->>AppGW: PayloadResolved / PayloadSettled
  else promise not resolved / timeout
    Watcher-->>Watcher: PromiseNotResolved
    Watcher->>Fees: unblockCredits(payloadId)
    Watcher-->>AppGW: PayloadCancelled
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

This should be reviewed thoroughly. Pay extra attention to:

  • Storage and upgrade safety:
    • contracts/evmx/base/AppGatewayBase.sol — storage removed/reshuffled (overrideParams, forwarderAddresses, currentPayloadId); confirm proxy/layout compatibility.
    • contracts/evmx/watcher/Watcher.sol & contracts/evmx/watcher/Configurations.sol — large state surface changes, new public variables and signature validation; verify gaps and access controls.
    • contracts/utils/common/Structs.sol & contracts/utils/common/IdUtils.sol — struct layout and payloadId encoding/decoding affect ABI across many contracts.
  • Financial/funds logic:
    • contracts/evmx/fees/FeesManager.sol, contracts/evmx/fees/Credit.sol, contracts/evmx/plugs/FeesPlug.sol — credit blocking/unblocking, consumeFrom resolution and transfer semantics; confirm invariants and reentrancy protections.
  • Cross-contract call paths / signer verification:
    • MessageSwitchboard and Socket flow (payload verification via payloadId): ensure create/decode and verification logic match in all callsites.
  • Interface & API changes:
    • contracts/evmx/interfaces/, contracts/protocol/interfaces/ — many signature changes (uint64→uint32, bytes32 payloadId, RawPayload usage); ensure all implementations and external callers updated.
  • Tests & deployment scripts:
    • hardhat-scripts/deploy/6.connect.ts and signer changes (WatcherMultiCallParams) — verify encoded calldata and signature verification consume the new types.

Possibly related PRs

Poem

RawPayloads stride with bytes32 names,
OverrideParams stitched to tame the games.
Watcher rings the bell, promises deployed,
Credits held then freed — state reconciled and buoyed.
New IDs, new paths — one lifecycle framed.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Phase 1' is vague and generic, providing no meaningful information about what this large changeset accomplishes. Replace with a descriptive title that summarizes the main architectural shift, e.g., 'Refactor payload-based architecture: migrate from request counters to payload IDs' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch phase-1

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

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: 19

Caution

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

⚠️ Outside diff range comments (6)
deprecated/PromiseResolver.sol (2)

57-60: Missing import for DeadlinePassed() causes compile error

DeadlinePassed() is used but not imported. This should import the symbol from Errors.sol.

-import {DeadlineNotPassedForOnChainRevert} from "../../utils/common/Errors.sol";
+import {DeadlineNotPassedForOnChainRevert, DeadlinePassed} from "../../utils/common/Errors.sol";

45-51: updateRequest signature mismatch in PromiseResolver
The call to updateRequest(requestCount) in deprecated/PromiseResolver.sol must include the feesUsed argument required by its definition (uint40,uint256); for example:

-                requestHandler__().updateRequest(requestCount);
+                requestHandler__().updateRequest(requestCount, 0);
contracts/evmx/fees/Credit.sol (1)

140-149: Critical: incorrect minting/accounting when native withdraw fails

On withdraw failure you mint creditAmount_ again, then set nativeAmount_ = 0 before adding it back. This double-mints credits and loses the native amount.

This should mint nativeAmount_ as credits and update event values correctly.

Apply:

-            if (!success) {
-                _mint(depositTo_, creditAmount_);
-                nativeAmount_ = 0;
-                creditAmount_ += nativeAmount_;
-            }
+            if (!success) {
+                // convert native to credits
+                _mint(depositTo_, nativeAmount_);
+                creditAmount_ += nativeAmount_;
+                nativeAmount_ = 0;
+            }
contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (1)

140-145: Update resolvePayload to match the new interface.

IPrecompile.resolvePayload now expects Payload calldata, but this override still takes PayloadParams calldata. This should be updated or the contract will no longer compile against the interface and the watcher cannot resolve scheduled payloads. Apply the signature change and forward the new struct fields:

-    function resolvePayload(PayloadParams calldata payloadParams_) external onlyRequestHandler {
-        (, uint256 executeAfter) = abi.decode(payloadParams_.precompileData, (uint256, uint256));
-
-        if (executeAfter > block.timestamp) revert ResolvingScheduleTooEarly();
-        emit ScheduleResolved(payloadParams_.payloadId);
-    }
+    function resolvePayload(Payload calldata payload) external onlyRequestHandler {
+        (, uint256 executeAfter) = abi.decode(payload.precompileData, (uint256, uint256));
+
+        if (executeAfter > block.timestamp) revert ResolvingScheduleTooEarly();
+        emit ScheduleResolved(payload.payloadId);
+    }
contracts/evmx/watcher/Configurations.sol (1)

45-122: Restore initialization of owner and watcher resolver.

The earlier version called _initializeWatcher and _initializeOwner during deployment. After dropping WatcherBase/Initializable there is no path that sets the owner or the address resolver, so every onlyOwner/onlyWatcher entry point will revert. This should reinstate an initializer/constructor that seeds the resolver + calls _initializeOwner(owner_) before these modifiers are used.

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

12-76: Fix enums and promise wiring to match the new OverrideParams shape.

Line 12 still imports IsPlug, Read, and Parallel, but those enums were removed from Structs.sol. This should drop the imports, otherwise compilation fails. Lines 67-88 manipulate overrideParams.isAsyncModifierSet, yet OverrideParams no longer contains that field. Replace this with whatever flag now lives alongside the override state (or remove the flag entirely) so the code compiles and actually reflects the new override lifecycle. Finally, Line 75 sets isValidPromise[promise_] = true even though promise_ is undefined after the previous retrieval was commented out. This should reinstate the watcher call (or otherwise provide the promise address) before assigning, or _postAsync will not compile.

🧹 Nitpick comments (13)
deprecated/RequestHandler.sol (1)

92-98: getPrecompileFees should validate callType

Without a guard, this will call .getPrecompileFees on address(0) and revert ambiguously. This should revert with InvalidCallType when unset.

     function getPrecompileFees(
         bytes4 callType_,
         bytes memory precompileData_
     ) external view returns (uint256) {
-        return precompiles[callType_].getPrecompileFees(precompileData_);
+        if (address(precompiles[callType_]) == address(0)) revert InvalidCallType();
+        return precompiles[callType_].getPrecompileFees(precompileData_);
     }
contracts/evmx/interfaces/IFeesManager.sol (1)

3-3: Trim unused imports

The interface does not use WriteFinality, AppGatewayApprovals, OverrideParams, Transaction, RawPayload, RequestParams. This should import only what’s needed to keep the surface lean.

-import {WriteFinality, AppGatewayApprovals, OverrideParams, Transaction, RawPayload, RequestParams} from "../../utils/common/Structs.sol";
+// No struct types referenced; drop unused imports
contracts/evmx/interfaces/IAuctionManager.sol (1)

4-4: Remove unused Structs imports

This interface does not reference RawPayload, OverrideParams, Transaction, Bid, RequestParams. This should remove the import.

-import {RawPayload, OverrideParams, Transaction, Bid, RequestParams} from "../../utils/common/Structs.sol";
+// No struct types referenced; drop unused imports
FunctionSignatures.md (1)

482-482: Rename noted; add deprecation note and cross-reference

updateRequest replaced updateRequestAndProcessBatch but retains the same selector 0x46464471. This should be called out explicitly to avoid confusion in external tooling and off-chain decoders. Add a deprecation note for the old name and a brief mapping.

contracts/evmx/fees/Credit.sol (1)

274-283: Variable name shadows type; use lowerCamelCase for clarity

Declaring RawPayload memory RawPayload; shadows the type and hurts readability. This should be rawPayload.

Apply:

-        RawPayload memory RawPayload;
-        RawPayload.overrideParams = overrideParams;
-        RawPayload.transaction = Transaction({
+        RawPayload memory rawPayload;
+        rawPayload.overrideParams = overrideParams;
+        rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
-        RawPayload.switchboardType = sbType;
-        watcher__().queue(RawPayload, address(this));
+        rawPayload.switchboardType = sbType;
+        watcher__().queue(rawPayload, address(this));
test/evmx/Watcher.t.sol (2)

264-265: Fix variable shadowing of type name

RawPayload[] memory RawPayload shadows the type and reduces readability. This should use a lowerCamelCase identifier and propagate to call sites.

-        RawPayload[] memory RawPayload = new RawPayload[](0);
+        RawPayload[] memory rawPayloads = new RawPayload[](0);
@@
-            RawPayload,
+            rawPayloads,
@@
-            RawPayload,
+            rawPayloads,

Also applies to: 272-273, 283-284


402-418: Test name casing and intent

Consider renaming to testRequestHandlerUpdateRequest for consistency with other test names. No functional change needed.

test/mock/MockWatcherPrecompile.sol (1)

26-29: Normalize parameter names (no type-name shadowing)

Use lowerCamelCase for parameters to avoid shadowing type names and improve readability.

-    function queue(
-        RawPayload calldata RawPayload_,
-        address appGateway_
-    ) external override returns (address, uint40) {}
+    function queue(
+        RawPayload calldata rawPayload_,
+        address appGateway_
+    ) external override returns (address, uint40) {}
@@
-    function queueAndSubmit(
-        RawPayload memory queue_,
+    function queueAndSubmit(
+        RawPayload memory rawPayload_,
         uint256 maxFees,
         address auctionManager,
         address consumeFrom,
         bytes calldata onCompleteData
     ) external override returns (uint40 requestCount, address[] memory promises) {}

Also applies to: 40-46

contracts/evmx/interfaces/IAppGateway.sol (1)

13-16: Update docstring to match API

Comment still says “Gets the override parameters”. This should reflect ForwarderParams.

-    /// @notice Gets the override parameters
-    /// @return overrideParams_ The override parameters
+    /// @notice Gets forwarder parameters (including override params for payload execution)
+    /// @return params The forwarder parameters
     function getForwarderParams() external view returns (ForwarderParams memory);
contracts/evmx/interfaces/IPrecompile.sol (1)

14-23: Doc/param mismatch in handlePayload

handlePayload now takes (rawPayload, appGateway, payloadId) but docs only describe rawPayload. This should document all params and returns.

-    /// @notice Handles payload processing and returns processed payload
-    /// @param rawPayload The payload parameters to handle
-    /// @return fees The fees required for processing
-    /// @return deadline The deadline for processing
-    /// @return precompileData The encoded precompile data
+    /// @notice Handles payload processing and returns computed fees, deadline and encoded precompile data
+    /// @param rawPayload The raw payload to handle (includes override params and transaction)
+    /// @param appGateway The app gateway invoking/preparing the payload
+    /// @param payloadId The unique payload identifier
+    /// @return fees The fees required for processing
+    /// @return deadline The deadline for processing
+    /// @return precompileData The encoded precompile data
contracts/evmx/helpers/Forwarder.sol (1)

93-106: Rename RawPayload variable in Forwarder.sol to rawPayload

-        RawPayload memory RawPayload;
-        RawPayload.overrideParams = forwarderParams.overrideParams;
-        RawPayload.transaction = Transaction({
+        RawPayload memory rawPayload;
+        rawPayload.overrideParams = forwarderParams.overrideParams;
+        rawPayload.transaction = Transaction({
             chainSlug: chainSlug,
             target: getOnChainAddress(),
             payload: msg.data
         });
-        watcher__().executePayload(
+        watcher__().executePayload(
             forwarderParams.overrideParams.maxFees,
             forwarderParams.overrideParams.consumeFrom,
             msgSender,
-            RawPayload
+            rawPayload
         );
deprecated/Watcher.sol (1)

28-38: Validate core contract addresses before setting

Zero addresses here brick the instance at runtime. This should enforce non-zero checks.

     function setCoreContracts(
         address requestHandler_,
         address configManager_,
         address promiseResolver_
     ) external onlyOwner {
+        require(requestHandler_ != address(0) && configManager_ != address(0) && promiseResolver_ != address(0), "zero addr");
         requestHandler__ = IRequestHandler(requestHandler_);
         configurations__ = IConfigurations(configManager_);
         promiseResolver__ = IPromiseResolver(promiseResolver_);
contracts/evmx/watcher/Watcher.sol (1)

176-182: Modifier name suggests wrong polarity

increaseFees and cancelRequest use isPayloadCancelled(payloadId_) while they proceed to cancel or modify fees. This should guard on “not cancelled” or a differently named modifier; confirm semantics to avoid logic bugs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a332094 and 6356eb5.

📒 Files selected for processing (26)
  • FunctionSignatures.md (1 hunks)
  • contracts/evmx/base/AppGatewayBase.sol (8 hunks)
  • contracts/evmx/fees/Credit.sol (1 hunks)
  • contracts/evmx/helpers/Forwarder.sol (2 hunks)
  • contracts/evmx/interfaces/IAppGateway.sol (2 hunks)
  • contracts/evmx/interfaces/IAuctionManager.sol (1 hunks)
  • contracts/evmx/interfaces/IFeesManager.sol (1 hunks)
  • contracts/evmx/interfaces/IPrecompile.sol (2 hunks)
  • contracts/evmx/interfaces/IRequestHandler.sol (1 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Configurations.sol (2 hunks)
  • contracts/evmx/watcher/RequestHandler.sol (0 hunks)
  • contracts/evmx/watcher/Watcher.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/ReadPrecompile.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (2 hunks)
  • contracts/utils/OverrideParamsLib.sol (1 hunks)
  • contracts/utils/common/Structs.sol (3 hunks)
  • deprecated/AuctionManager.sol (1 hunks)
  • deprecated/Configurations.sol (1 hunks)
  • deprecated/ForwarderV2.sol (1 hunks)
  • deprecated/PromiseResolver.sol (1 hunks)
  • deprecated/RequestHandler.sol (1 hunks)
  • deprecated/Watcher.sol (1 hunks)
  • test/evmx/Watcher.t.sol (4 hunks)
  • test/mock/MockWatcherPrecompile.sol (2 hunks)
💤 Files with no reviewable changes (1)
  • contracts/evmx/watcher/RequestHandler.sol
🔇 Additional comments (1)
contracts/utils/OverrideParamsLib.sol (1)

12-26: Fluent builder looks solid; defaults are reasonable

clear() initializes sensible defaults and threads switchboardType. Setters correctly mutate memory and return self.

Comment on lines +26 to 28
/// @param payload The payload parameters to resolve
function resolvePayload(Payload calldata payload) external;
}
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

🧩 Analysis chain

Align resolvePayload docs with new type

Good switch to Payload; ensure implementers are updated.

Verify all IPrecompile implementers use new signatures:


🏁 Script executed:

#!/bin/bash
# List contracts implementing IPrecompile and check handlePayload/resolvePayload signatures
rg -n -C2 -P 'contract\s+\w+\s+is\s+[^}]*\bIPrecompile\b' contracts
rg -n -C2 -P '\bfunction\s+handlePayload\s*\(' contracts | rg -n -C0 -P 'RawPayload'
rg -n -C2 -P '\bfunction\s+resolvePayload\s*\(' contracts | rg -n -C0 -P 'Payload'

Length of output: 2693


Fix resolvePayload signature in IPrecompile implementers

  • WritePrecompile.sol:246 — replace PayloadParams calldata payloadParams_ with Payload calldata payload
  • SchedulePrecompile.sol:140 — replace PayloadParams calldata payloadParams_ with Payload calldata payload
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IPrecompile.sol around lines 26 to 28, implementers
have mismatched function signatures: replace the parameter type and name in
WritePrecompile.sol at line ~246 and SchedulePrecompile.sol at line ~140 from
"PayloadParams calldata payloadParams_" to "Payload calldata payload"; update
the function declaration to match the interface, rename occurrences of
payloadParams_ to payload within each file (including any field accesses), and
ensure any imports or type aliases for Payload are present so the signatures and
internal references compile.

Comment on lines 21 to 27
function submitRequest(
uint256 maxFees_,
address auctionManager_,
address consumeFrom_,
address appGateway_,
QueueParams[] calldata queueParams_,
RawPayload calldata RawPayload_,
bytes memory onCompleteData_
) external returns (uint40 requestCount, address[] memory promiseList);
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

Align submitRequest with ForwarderParams.

IWatcher.submitRequest now forwards a ForwarderParams struct, so this interface still demanding the old (maxFees_, consumeFrom_, appGateway_) tuple will no longer compile or link. This should switch to the new struct signature (and stick to calldata to avoid copies):

-    function submitRequest(
-        uint256 maxFees_,
-        address consumeFrom_,
-        address appGateway_,
-        RawPayload calldata RawPayload_,
-        bytes memory onCompleteData_
-    ) external returns (uint40 requestCount, address[] memory promiseList);
+    function submitRequest(
+        ForwarderParams calldata forwarderParams_,
+        RawPayload calldata rawPayload_,
+        bytes calldata onCompleteData_
+    ) external returns (uint40 requestCount, address[] memory promiseList);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function submitRequest(
uint256 maxFees_,
address auctionManager_,
address consumeFrom_,
address appGateway_,
QueueParams[] calldata queueParams_,
RawPayload calldata RawPayload_,
bytes memory onCompleteData_
) external returns (uint40 requestCount, address[] memory promiseList);
function submitRequest(
ForwarderParams calldata forwarderParams_,
RawPayload calldata rawPayload_,
bytes calldata onCompleteData_
) external returns (uint40 requestCount, address[] memory promiseList);
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IRequestHandler.sol around lines 21 to 27, the
submitRequest signature still expects the old (maxFees_, consumeFrom_,
appGateway_) tuple; update it to accept the ForwarderParams struct as calldata
and use calldata for the onCompleteData to avoid copying: replace the three
separate parameters with ForwarderParams calldata forwarderParams_ and change
bytes memory onCompleteData_ to bytes calldata onCompleteData_, keeping the
return types the same so the interface matches the new IWatcher.forwarder-based
call site.

Comment on lines 175 to 188
if (rawPayload_.overrideParams.gasLimit == 0) {
if (rawPayload_.transaction.chainSlug == 5000) {
// Mantle default gas limit
gasLimit = 8_000_000_000;
r else if (rawPayload_.transaction.chainSlug == 1329) {
// Sei default gas limit
gasLimit = 8_000_000;
r else if (rawPayload_.transaction.chainSlug == 999) {
// HyperEVM default gas limit
gasLimit = 1_500_000;
} else {
gasLimit = 10_000_000; // other chains default gas limit
}
}
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

Return the correct gas limit.

Line 175 only sets gasLimit if the override value is zero. When a caller provides a positive overrideParams.gasLimit, this function returns the default-initialized 0, which later flows into precompileData, DigestParams, and ultimately the digest hash. This should assign gasLimit = rawPayload_.overrideParams.gasLimit up front and then overwrite it only when the override was zero; otherwise every request will be digested with an incorrect gas limit. While fixing that, also clean up the r else if tokens on Lines 179 and 183—those typos currently break compilation.

🤖 Prompt for AI Agents
In contracts/evmx/watcher/precompiles/WritePrecompile.sol around lines 175 to
188, the function never assigns gasLimit from
rawPayload_.overrideParams.gasLimit so a positive override is ignored and zero
is returned into the digest; also two occurrences of "r else if" are typos that
break compilation. Fix by initializing gasLimit =
rawPayload_.overrideParams.gasLimit before the conditional, then only enter the
chainSlug-based defaults when overrideParams.gasLimit == 0; remove the stray
leading "r" characters so the conditional branches read "else if" correctly.

Comment on lines +107 to +136
function submitRequest(
uint256 maxFees_,
address consumeFrom_,
address appGateway_,
QueueParams calldata queueParams_,
bytes memory onCompleteData_
) external onlyWatcher returns (uint40 requestCount, address[] memory promiseList) {
if (!feesManager__().isCreditSpendable(consumeFrom_, appGateway_, maxFees_))
revert InsufficientFees();

RequestParams storage r = _requests[requestCount];
r.requestFeesDetails.maxFees = maxFees_;
r.requestFeesDetails.consumeFrom = consumeFrom_;
requestCount = nextRequestCount++;

bytes4 callType = queueParams_.overrideParams.callType;

(PayloadParams memory p, uint256 estimatedFees) = _createRequest(
queueParams_,
appGateway_,
requestCount
);

if (estimatedFees > maxFees_) revert InsufficientMaxFees();
feesManager__().blockCredits(requestCount, r.requestFeesDetails.consumeFrom, estimatedFees);
r.payloadId = p.payloadId;
_processBatch(r);

emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p);
}
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

submitRequest uses requestCount before assignment; appGateway not stored; promiseList uninitialized

  • r is taken from _requests[requestCount] before requestCount is set → writes to index 0.
  • r.appGateway is never set, breaking later checks (OnlyAppGateway).
  • promiseList is a named return but never assigned → uninitialized return.

This should set requestCount first, then r, store appGateway, and initialize promiseList.

-        RequestParams storage r = _requests[requestCount];
-        r.requestFeesDetails.maxFees = maxFees_;
-        r.requestFeesDetails.consumeFrom = consumeFrom_;
-        requestCount = nextRequestCount++;
+        requestCount = nextRequestCount++;
+        RequestParams storage r = _requests[requestCount];
+        r.requestFeesDetails.maxFees = maxFees_;
+        r.requestFeesDetails.consumeFrom = consumeFrom_;
+        r.appGateway = appGateway_;
@@
-        emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p);
+        // No promises in deprecated flow; return empty list for ABI correctness
+        promiseList = new address[](0);
+        emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function submitRequest(
uint256 maxFees_,
address consumeFrom_,
address appGateway_,
QueueParams calldata queueParams_,
bytes memory onCompleteData_
) external onlyWatcher returns (uint40 requestCount, address[] memory promiseList) {
if (!feesManager__().isCreditSpendable(consumeFrom_, appGateway_, maxFees_))
revert InsufficientFees();
RequestParams storage r = _requests[requestCount];
r.requestFeesDetails.maxFees = maxFees_;
r.requestFeesDetails.consumeFrom = consumeFrom_;
requestCount = nextRequestCount++;
bytes4 callType = queueParams_.overrideParams.callType;
(PayloadParams memory p, uint256 estimatedFees) = _createRequest(
queueParams_,
appGateway_,
requestCount
);
if (estimatedFees > maxFees_) revert InsufficientMaxFees();
feesManager__().blockCredits(requestCount, r.requestFeesDetails.consumeFrom, estimatedFees);
r.payloadId = p.payloadId;
_processBatch(r);
emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p);
}
function submitRequest(
uint256 maxFees_,
address consumeFrom_,
address appGateway_,
QueueParams calldata queueParams_,
bytes memory onCompleteData_
) external onlyWatcher returns (uint40 requestCount, address[] memory promiseList) {
if (!feesManager__().isCreditSpendable(consumeFrom_, appGateway_, maxFees_))
revert InsufficientFees();
// Assign the new requestCount before accessing the storage slot
requestCount = nextRequestCount++;
RequestParams storage r = _requests[requestCount];
r.requestFeesDetails.maxFees = maxFees_;
r.requestFeesDetails.consumeFrom = consumeFrom_;
r.appGateway = appGateway_;
bytes4 callType = queueParams_.overrideParams.callType;
(PayloadParams memory p, uint256 estimatedFees) = _createRequest(
queueParams_,
appGateway_,
requestCount
);
if (estimatedFees > maxFees_) revert InsufficientMaxFees();
feesManager__().blockCredits(requestCount, r.requestFeesDetails.consumeFrom, estimatedFees);
r.payloadId = p.payloadId;
_processBatch(r);
// No promises in deprecated flow; return empty list for ABI correctness
promiseList = new address[](0);
emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p);
}
🤖 Prompt for AI Agents
In deprecated/RequestHandler.sol around lines 107 to 136, the function uses
requestCount before assigning it, never sets r.appGateway, and returns an
uninitialized promiseList; fix by assigning requestCount = nextRequestCount++
immediately at the top (before accessing _requests), then load RequestParams
storage r = _requests[requestCount]; set r.appGateway = appGateway_ (and keep
the existing r.requestFeesDetails assignments), ensure fees are estimated and
blocked as before, call _processBatch(r), and initialize promiseList (e.g., an
empty address[] with length 0 or populated as needed) before returning so the
named return is valid and OnlyAppGateway checks later will see the stored
appGateway.

Comment on lines +234 to +246
function updateRequest(
uint40 requestCount_,
uint256 feesUsed_
) external onlyPromiseResolver isRequestCancelled(requestCount_) {
RequestParams storage r = _requests[requestCount_];
PayloadParams storage payloadParams = _payloads[r.payloadId];
payloadParams.resolvedAt = block.timestamp;

IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams);
r.isRequestExecuted = true;
_settleRequest(requestCount_, r);
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

updateRequest extra parameter is unused and diverges from caller

feesUsed_ is unused here and PromiseResolver calls the single‑arg variant. This should be reconciled to avoid dead params and interface drift.

Option A (align to new API):

-    function updateRequest(
-        uint40 requestCount_,
-        uint256 feesUsed_
-    ) external onlyPromiseResolver isRequestCancelled(requestCount_) {
+    function updateRequest(
+        uint40 requestCount_
+    ) external onlyPromiseResolver isRequestCancelled(requestCount_) {
         RequestParams storage r = _requests[requestCount_];
         PayloadParams storage payloadParams = _payloads[r.payloadId];
         payloadParams.resolvedAt = block.timestamp;

         IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams);
         r.isRequestExecuted = true;
         _settleRequest(requestCount_, r);
     }

Option B (temporary compatibility): keep signature and pass 0 from PromiseResolver, then remove feesUsed_ in a follow‑up.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function updateRequest(
uint40 requestCount_,
uint256 feesUsed_
) external onlyPromiseResolver isRequestCancelled(requestCount_) {
RequestParams storage r = _requests[requestCount_];
PayloadParams storage payloadParams = _payloads[r.payloadId];
payloadParams.resolvedAt = block.timestamp;
IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams);
r.isRequestExecuted = true;
_settleRequest(requestCount_, r);
}
function updateRequest(
uint40 requestCount_
) external onlyPromiseResolver isRequestCancelled(requestCount_) {
RequestParams storage r = _requests[requestCount_];
PayloadParams storage payloadParams = _payloads[r.payloadId];
payloadParams.resolvedAt = block.timestamp;
IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams);
r.isRequestExecuted = true;
_settleRequest(requestCount_, r);
}
🤖 Prompt for AI Agents
In deprecated/RequestHandler.sol around lines 234–246 the updateRequest function
declares an unused feesUsed_ parameter that diverges from the PromiseResolver
caller; remove the unused feesUsed_ parameter from updateRequest's signature and
any related interface declarations, then update all callers (including
PromiseResolver) to call the new single-argument variant
(updateRequest(requestCount_)); ensure compiler/interfaces and any tests are
updated accordingly and run a full compile/test to confirm no remaining
references to feesUsed_ exist.

Comment on lines +101 to +114
function _submitRequest(
uint256 maxFees,
address,
address consumeFrom,
bytes memory onCompleteData
) internal returns (uint40 requestCount, address[] memory promiseList) {
if (payloadQueue.length == 0) return (0, new address[](0));
address appGateway = msg.sender;

// this check is to verify that msg.sender (app gateway base) belongs to correct app gateway
if (appGateway != appGatewayTemp) revert InvalidAppGateway();
latestAsyncPromise = address(0);
appGatewayTemp = address(0);

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

Early return leaves appGatewayTemp/last promise set → deadlock on subsequent calls

If payloadQueue is empty, _submitRequest returns without clearing appGatewayTemp/latestAsyncPromise. Any later queue/submit attempts will be blocked. This should reset temporary state before returning.

-        if (payloadQueue.length == 0) return (0, new address[](0));
+        if (payloadQueue.length == 0) {
+            // reset temp state to avoid lock-up if queue was cleared externally
+            latestAsyncPromise = address(0);
+            appGatewayTemp = address(0);
+            return (0, new address[](0));
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _submitRequest(
uint256 maxFees,
address,
address consumeFrom,
bytes memory onCompleteData
) internal returns (uint40 requestCount, address[] memory promiseList) {
if (payloadQueue.length == 0) return (0, new address[](0));
address appGateway = msg.sender;
// this check is to verify that msg.sender (app gateway base) belongs to correct app gateway
if (appGateway != appGatewayTemp) revert InvalidAppGateway();
latestAsyncPromise = address(0);
appGatewayTemp = address(0);
function _submitRequest(
uint256 maxFees,
address,
address consumeFrom,
bytes memory onCompleteData
) internal returns (uint40 requestCount, address[] memory promiseList) {
if (payloadQueue.length == 0) {
// reset temp state to avoid lock-up if queue was cleared externally
latestAsyncPromise = address(0);
appGatewayTemp = address(0);
return (0, new address[](0));
}
address appGateway = msg.sender;
// this check is to verify that msg.sender (app gateway base) belongs to correct app gateway
if (appGateway != appGatewayTemp) revert InvalidAppGateway();
latestAsyncPromise = address(0);
appGatewayTemp = address(0);
// ... rest of function ...
}
🤖 Prompt for AI Agents
In deprecated/Watcher.sol around lines 101 to 114, the early return when
payloadQueue.length == 0 leaves appGatewayTemp and latestAsyncPromise uncleared
causing subsequent calls to deadlock; modify the function so that before
returning (0, new address[](0)) you reset latestAsyncPromise = address(0) and
appGatewayTemp = address(0) (same cleanup performed later in the function) to
ensure temporary state is always cleared on the empty-queue path.

Comment on lines +126 to +130
/// @notice Clears the call parameters array
function clearQueue() public {
delete payloadQueue;
}

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

clearQueue is public and unprotected → anyone can nuke pending work (and cause the deadlock above)

This should not be externally callable. Restrict or make internal to eliminate DoS.

-    function clearQueue() public {
+    function clearQueue() internal {
         delete payloadQueue;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Clears the call parameters array
function clearQueue() public {
delete payloadQueue;
}
/// @notice Clears the call parameters array
function clearQueue() internal {
delete payloadQueue;
}
🤖 Prompt for AI Agents
In deprecated/Watcher.sol around lines 126 to 130, the clearQueue function is
declared public which allows anyone to delete the payloadQueue and cause a DoS;
change its visibility to internal or private (or add an access-control modifier
such as onlyOwner) so only the contract/operator can clear the queue, and update
any callers accordingly (or add an admin-only wrapper if external invocation is
needed); ensure the function signature and any modifiers are updated and that
tests reflect the new restricted access.

Comment on lines +212 to +224
if (contractAddress_ == address(0)) revert InvalidContract();
if (data_.length == 0) revert InvalidData();
if (signature_.length == 0) revert InvalidSignature();
if (isNonceUsed[nonce_]) revert NonceUsed();
isNonceUsed[nonce_] = true;

bytes32 digest = keccak256(
abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_)
);

// check if signature is valid
if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature();
}
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

Nonce is marked used before signature validation → trivially DoS valid signatures

Move isNonceUsed assignment after signature verification.

-        if (isNonceUsed[nonce_]) revert NonceUsed();
-        isNonceUsed[nonce_] = true;
+        if (isNonceUsed[nonce_]) revert NonceUsed();
         bytes32 digest = keccak256(
             abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_)
         );
-        // check if signature is valid
-        if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature();
+        // check if signature is valid
+        if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature();
+        // mark only after successful validation
+        isNonceUsed[nonce_] = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (contractAddress_ == address(0)) revert InvalidContract();
if (data_.length == 0) revert InvalidData();
if (signature_.length == 0) revert InvalidSignature();
if (isNonceUsed[nonce_]) revert NonceUsed();
isNonceUsed[nonce_] = true;
bytes32 digest = keccak256(
abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_)
);
// check if signature is valid
if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature();
}
if (contractAddress_ == address(0)) revert InvalidContract();
if (data_.length == 0) revert InvalidData();
if (signature_.length == 0) revert InvalidSignature();
if (isNonceUsed[nonce_]) revert NonceUsed();
bytes32 digest = keccak256(
abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_)
);
// check if signature is valid
if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature();
// mark only after successful validation
isNonceUsed[nonce_] = true;
}
🤖 Prompt for AI Agents
In deprecated/Watcher.sol around lines 212 to 224, the code marks
isNonceUsed[nonce_] = true before verifying the signature, allowing an attacker
to DoS valid signed requests by pre-marking the nonce; move the isNonceUsed
assignment to after the signature verification (i.e., only set
isNonceUsed[nonce_] = true once _recoverSigner(digest, signature_) == owner()
has succeeded), keep the same digest computation and revert paths, and ensure no
other early state changes occur before signature validation.

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: 10

Caution

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

⚠️ Outside diff range comments (3)
deprecated/test/evmx/Watcher.t.sol (2)

785-794: Fix self-referential local; compute new expiry from contract state

uint256 expiryTime = expiryTime + 100; reads an uninitialized local and will fail. Derive the new value from the contract’s current expiry, and use a distinct name.

-        uint256 expiryTime = expiryTime + 100;
+        uint256 newExpiryTime = schedulePrecompile.expiryTime() + 100;
         hoax(watcherAddress);
         vm.expectEmit(true, true, true, true);
-        emit SchedulePrecompile.ExpiryTimeSet(expiryTime);
-        schedulePrecompile.setExpiryTime(expiryTime);
+        emit SchedulePrecompile.ExpiryTimeSet(newExpiryTime);
+        schedulePrecompile.setExpiryTime(newExpiryTime);
...
-        schedulePrecompile.setExpiryTime(expiryTime);
+        schedulePrecompile.setExpiryTime(newExpiryTime);

400-416: Align updateRequest signature in test
Replace payloadId with a uint256 feesUsed value in updateRequest(requestCount, …) at Watcher.t.sol:409 and 415.

contracts/evmx/fees/Credit.sol (1)

139-151: Critical: double-mint bug when native transfer fails

If feesPool.withdraw fails, the code mints creditAmount_ again and then adds 0 to creditAmount_. This inflates credits incorrectly and misreports the event.

This should mint nativeAmount_ as credits and update the event values accordingly.

-            if (!success) {
-                _mint(depositTo_, creditAmount_);
-                nativeAmount_ = 0;
-                creditAmount_ += nativeAmount_;
-            }
+            if (!success) {
+                // Add failed native transfer as credits instead
+                _mint(depositTo_, nativeAmount_);
+                creditAmount_ += nativeAmount_;
+                nativeAmount_ = 0;
+            }
♻️ Duplicate comments (4)
deprecated/IRequestHandler.sol (1)

21-27: Align submitRequest to ForwarderParams and use calldata for bytes

This should switch to ForwarderParams calldata and bytes calldata to match the forwarder-based flow and avoid unnecessary copies. Already flagged earlier.

-    function submitRequest(
-        uint256 maxFees_,
-        address consumeFrom_,
-        address appGateway_,
-        RawPayload calldata RawPayload_,
-        bytes memory onCompleteData_
-    ) external returns (uint40 requestCount, address[] memory promiseList);
+    function submitRequest(
+        ForwarderParams calldata forwarderParams_,
+        RawPayload calldata rawPayload_,
+        bytes calldata onCompleteData_
+    ) external returns (uint40 requestCount, address[] memory promiseList);
contracts/evmx/fees/FeesManager.sol (1)

139-140: Missing state variable: overrideParams is undefined.

Line 140 accesses overrideParams.consumeFrom, but FeesManager does not declare an overrideParams state variable. This will cause a compilation error.

Apply the same fix as in unblockAndAssignCredits:

-        // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
-        address consumeFrom = overrideParams.consumeFrom;
+        address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

174-187: Gas limit not assigned when override is non-zero.

When rawPayload_.overrideParams.gasLimit is non-zero, the function never assigns it to the return value gasLimit, so the function returns 0. This incorrect value flows into precompileData (line 113), DigestParams (line 129), and ultimately the digest hash (line 139), breaking execution.

Apply this diff to assign the override value first, then apply chain-specific defaults only when needed:

+        gasLimit = rawPayload_.overrideParams.gasLimit;
         // todo: can be changed to set the default gas limit for each chain
-        if (rawPayload_.overrideParams.gasLimit == 0) {
+        if (gasLimit == 0) {
             if (rawPayload_.transaction.chainSlug == 5000) {
                 // Mantle default gas limit
                 gasLimit = 8_000_000_000;
             } else if (rawPayload_.transaction.chainSlug == 1329) {
                 // Sei default gas limit
                 gasLimit = 8_000_000;
             } else if (rawPayload_.transaction.chainSlug == 999) {
                 // HyperEVM default gas limit
                 gasLimit = 1_500_000;
             } else {
                 gasLimit = 10_000_000; // other chains default gas limit
             }
         }
contracts/evmx/watcher/Watcher.sol (1)

4-11: Add missing imports for referenced symbols.

The code references symbols not imported here:

  • fromBytes32Format (used at line 175) needs import from Converters.sol
  • AppGatewayAlreadyCalled and InvalidCallerTriggered errors (used at lines 173, 177) need import from Errors.sol
 import "solady/utils/Initializable.sol";
 import "./Configurations.sol";
 import {IPrecompile} from "../interfaces/IPrecompile.sol";
 import {IFeesManager} from "../interfaces/IFeesManager.sol";
 import {IPromise} from "../interfaces/IPromise.sol";
 import {IERC20} from "../interfaces/IERC20.sol";
 import "../../utils/common/IdUtils.sol";
+import {fromBytes32Format} from "../../utils/common/Converters.sol";
+import {AppGatewayAlreadyCalled, InvalidCallerTriggered} from "../../utils/common/Errors.sol";
 import "solady/utils/LibCall.sol";
🧹 Nitpick comments (2)
deprecated/IAuctionManager.sol (1)

4-4: Unused imported symbols and stale NatSpec.

This interface doesn’t use RawPayload/OverrideParams/Transaction/Bid/RequestParams. These should be removed to avoid noise. Also, NatSpec still says “request count” while the repo moves to payloadId; add a deprecation note or clarify intent.

-import {RawPayload, OverrideParams, Transaction, Bid, RequestParams} from "../../utils/common/Structs.sol";
+// (deprecated) no struct imports needed here
contracts/protocol/SocketBatcher.sol (1)

66-103: Remove dead/commented CCTP code or guard it behind a feature flag

Large commented blocks rot quickly and confuse maintenance. This should be deleted, or wrapped under a compile-time flag with tests documenting re-enablement criteria.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6356eb5 and 1de79fa.

📒 Files selected for processing (27)
  • contracts/evmx/base/AppGatewayBase.sol (7 hunks)
  • contracts/evmx/fees/Credit.sol (3 hunks)
  • contracts/evmx/fees/FeesManager.sol (5 hunks)
  • contracts/evmx/helpers/AddressResolver.sol (0 hunks)
  • contracts/evmx/helpers/AddressResolverUtil.sol (1 hunks)
  • contracts/evmx/helpers/AsyncPromise.sol (4 hunks)
  • contracts/evmx/helpers/Forwarder.sol (2 hunks)
  • contracts/evmx/interfaces/IAddressResolver.sol (0 hunks)
  • contracts/evmx/interfaces/IAppGateway.sol (1 hunks)
  • contracts/evmx/interfaces/IFeesManager.sol (2 hunks)
  • contracts/evmx/interfaces/IPromise.sol (1 hunks)
  • contracts/evmx/interfaces/IReceiver.sol (0 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Watcher.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/ReadPrecompile.sol (2 hunks)
  • contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (5 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (5 hunks)
  • contracts/protocol/SocketBatcher.sol (2 hunks)
  • contracts/utils/common/Errors.sol (1 hunks)
  • contracts/utils/common/IdUtils.sol (1 hunks)
  • contracts/utils/common/Structs.sol (3 hunks)
  • deprecated/IAuctionManager.sol (1 hunks)
  • deprecated/IRequestHandler.sol (1 hunks)
  • deprecated/IWatcher.sol (1 hunks)
  • deprecated/test/evmx/ProxyMigration.t.sol (1 hunks)
  • deprecated/test/evmx/Watcher.t.sol (4 hunks)
  • test/mock/MockWatcherPrecompile.sol (0 hunks)
💤 Files with no reviewable changes (4)
  • contracts/evmx/helpers/AddressResolver.sol
  • contracts/evmx/interfaces/IReceiver.sol
  • contracts/evmx/interfaces/IAddressResolver.sol
  • test/mock/MockWatcherPrecompile.sol
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/evmx/helpers/Forwarder.sol
🔇 Additional comments (19)
contracts/evmx/helpers/AddressResolverUtil.sol (2)

29-31: Remove incorrect delegation concern
The _isWatcher function has always compared account_ directly to watcher__(); no delegation logic existed or was removed.

Likely an incorrect or invalid review comment.


29-31: Confirm no remaining callers of deployForwarder__(). Ripgrep search across all .sol files returned no matches for deployForwarder__. Removal is safe.

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

81-87: Initializer versioning sanity-check.

initialize remains reinitializer(1). If prior deployments already used version 1, they won’t be able to call this initializer again post-upgrade. Confirm that AsyncPromise instances are clones initialized once per instance (not proxy-upgraded), or bump/init via a new versioned initializer if needed.


184-185: Validation change LGTM.

Comparing payloadId to watcher.currentPayloadId() matches the new model.

contracts/evmx/interfaces/IAppGateway.sol (1)

15-15: No breaking change detected – AppGatewayBase now matches the single-value signature and no callers destructure two returns.

deprecated/IRequestHandler.sol (1)

29-29: All updateRequest signatures are consistent: interface, implementation, and tests use (uint40, uint256); no bytes32 parameter found.

Likely an incorrect or invalid review comment.

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

143-147: RawPayload structure is correct; callers must use nested access.

The past review comment flagged a missing switchboardType field, but the struct definition is correct—switchboardType is accessible via rawPayload.overrideParams.switchboardType (line 129). The issue is in consuming code (e.g., WritePrecompile.sol line 117, ForwarderV2.sol line 101) that must access the nested field rather than a direct property.

This file's definitions align with the payload-centric model.

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

27-27: Override state properly centralized.

The overrideParams public state variable (line 27) provides a single source of truth for override configuration, avoiding scattered state. The getOverrideParams() (lines 201-203) and applyOverride() (lines 213-215) methods complete the fluent builder pattern enabled by OverrideParamsLib.

contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

115-118: Correct nested access to switchboardType.

Line 117 correctly accesses rawPayload.overrideParams.switchboardType instead of the non-existent rawPayload.switchboardType. This aligns with the RawPayload structure defined in Structs.sol (lines 143-147) where switchboardType is a field of the nested OverrideParams.

contracts/evmx/watcher/precompiles/ReadPrecompile.sol (1)

44-69: Clean RawPayload handling with proper access control.

The handlePayload function correctly uses RawPayload (line 45) and accesses nested fields like rawPayload.overrideParams.readAtBlockNumber (line 59) and rawPayload.transaction (lines 58, 65). The onlyWatcher modifier (line 50) properly gates access. This implementation aligns with the payload-centric model and mirrors the pattern in WritePrecompile.sol.

contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2)

28-33: LGTM!

The explicit watcher__ state variable and onlyWatcher modifier provide clear access control. This is more transparent than inheriting from WatcherBase.


117-145: Remove manual overflow guard: Solidity 0.8.21 uses checked arithmetic so block.timestamp + rawPayload.overrideParams.delayInSeconds reverts on overflow. Pattern is consistent across precompiles.

contracts/evmx/interfaces/IWatcher.sol (1)

40-49: LGTM!

The executePayload method signature correctly accepts RawPayload and appGateway_, aligning with the payload-centric model described in the PR objectives.

contracts/evmx/watcher/Watcher.sol (5)

153-170: Fix inverted deadline validation in markRevert.

Line 160 checks if (payloadParams.deadline > block.timestamp) which reverts when the deadline is in the future. This is backwards—you should only allow marking revert when the deadline has passed.

-        if (payloadParams.deadline > block.timestamp) revert DeadlineNotPassedForOnChainRevert();
+        if (payloadParams.deadline >= block.timestamp) revert DeadlineNotPassedForOnChainRevert();

Or if the intent is to require the deadline to have already passed before allowing onchain revert:

-        if (payloadParams.deadline > block.timestamp) revert DeadlineNotPassedForOnChainRevert();
+        if (payloadParams.deadline < block.timestamp) revert DeadlineAlreadyPassed();

Clarify the intended behavior and fix the logic accordingly.

Likely an incorrect or invalid review comment.


240-246: switchboards mapping key type correct – declared as mapping(uint32 => mapping(bytes32 => uint64)) in Configurations.sol, matching bytes32 switchboardType_.


203-223: unblockCredits signature matches interface – no change needed
unblockCredits(bytes32 payloadId) in IFeesManager and FeesManager.sol accepts only the payloadId, so the call is correct.

Likely an incorrect or invalid review comment.


93-108: Approve payloadPointer synchronization and initialization
The if (nextPayloadCount == 0) nextPayloadCount = 1 guard ensures pointers start at 1, and payloadPointer: nextPayloadCount++ aligns with the emitted currentPayloadId.


70-109: Approve executePayload ordering
currentPayloadId is assigned at lines 79–82 before calling blockCredits on line 84.

deprecated/IWatcher.sol (1)

10-89: Deprecated interface isn't used in active code Confirmed no non-deprecated Solidity files import deprecated/IWatcher.sol.

Comment on lines 270 to 281
// applyOverride(
// OverrideParamsLib.setMaxFees(overrideParams, getMaxFees(chainSlug_)).setConsumeFrom(consumeFrom_)
// );
RawPayload memory rawPayload;
rawPayload.overrideParams = overrideParams;
rawPayload.transaction = Transaction({
chainSlug: chainSlug_,
target: _getFeesPlugAddress(chainSlug_),
payload: payload_
});
queueParams.switchboardType = sbType;
watcher__().queue(queueParams, address(this));
watcher__().executePayload(rawPayload, address(this));
}
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

Construct and apply override params before executing payload

overrideParams is used but never constructed; applyOverride(...) is commented out. This leaves consumeFrom/maxFees unset, likely breaking fee accounting/execution.

This should set overrideParams locally with consumeFrom_ and maxFees_ (and any defaults), assign it to rawPayload.overrideParams, then call executePayload.

-        // applyOverride(
-        //     OverrideParamsLib.setMaxFees(overrideParams, getMaxFees(chainSlug_)).setConsumeFrom(consumeFrom_)
-        // );
-        RawPayload memory rawPayload;
-        rawPayload.overrideParams = overrideParams;
+        RawPayload memory rawPayload;
+        OverrideParams memory params;
+        // Populate override params locally to avoid stale/global state
+        params = OverrideParamsLib
+            .setMaxFees(params, getMaxFees(chainSlug_))
+            .setConsumeFrom(consumeFrom_);
+        rawPayload.overrideParams = params;
         rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
         watcher__().executePayload(rawPayload, address(this));

Add missing import at the top of the file:

+import {OverrideParams} from "../../utils/common/Structs.sol";
+import "../../utils/OverrideParamsLib.sol";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// applyOverride(
// OverrideParamsLib.setMaxFees(overrideParams, getMaxFees(chainSlug_)).setConsumeFrom(consumeFrom_)
// );
RawPayload memory rawPayload;
rawPayload.overrideParams = overrideParams;
rawPayload.transaction = Transaction({
chainSlug: chainSlug_,
target: _getFeesPlugAddress(chainSlug_),
payload: payload_
});
queueParams.switchboardType = sbType;
watcher__().queue(queueParams, address(this));
watcher__().executePayload(rawPayload, address(this));
}
// Add these at the top of the file:
import {OverrideParams} from "../../utils/common/Structs.sol";
import "../../utils/OverrideParamsLib.sol";
RawPayload memory rawPayload;
OverrideParams memory params;
// Populate override params locally to avoid stale/global state
params = OverrideParamsLib
.setMaxFees(params, getMaxFees(chainSlug_))
.setConsumeFrom(consumeFrom_);
rawPayload.overrideParams = params;
rawPayload.transaction = Transaction({
chainSlug: chainSlug_,
target: _getFeesPlugAddress(chainSlug_),
payload: payload_
});
watcher__().executePayload(rawPayload, address(this));
}

Comment on lines 117 to 118
// address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
address consumeFrom = overrideParams.consumeFrom;
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

Missing state variable: overrideParams is undefined.

Line 118 accesses overrideParams.consumeFrom, but FeesManager does not declare an overrideParams state variable. This will cause a compilation error. The consumeFrom address must be either:

  1. Passed as a function parameter to unblockAndAssignCredits, or
  2. Retrieved from payload storage (e.g., watcher__.getPayload(payloadId_).consumeFrom), or
  3. Inherited from a base contract that declares overrideParams

Apply this diff to retrieve consumeFrom from the watcher's payload storage:

-        // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
-        address consumeFrom = overrideParams.consumeFrom;
+        address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;

Alternatively, add consumeFrom_ as a function parameter if the caller has this information.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
address consumeFrom = overrideParams.consumeFrom;
// address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
🤖 Prompt for AI Agents
In contracts/evmx/fees/FeesManager.sol around lines 117-118 the code references
overrideParams.consumeFrom which is not declared and causes a compile error;
replace that reference by retrieving the value from the watcher payload (e.g.
use watcher__.getPayload(payloadId_).requestFeesDetails.consumeFrom) or
alternatively add consumeFrom_ as a parameter to unblockAndAssignCredits and use
that parameter; ensure the chosen approach removes the undefined overrideParams
usage and updates function signature/visibility and any callers accordingly.

Comment on lines +26 to 28
/// @notice The payload count of the promise
bytes32 public override payloadId;

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

CRITICAL: Storage layout break (packed-slot widened).

Replacing a packed uint40 with bytes32 in-place shifts localInvoker and all subsequent variables into new slots. This will corrupt state on upgrades/clones relying on the old layout. This must be fixed before merge.

Preserve the original packed layout and append payloadId at the end (before the gap). Deprecate the old field to keep the slot intact.

Apply within this segment to restore the packed slot:

-    /// @notice The payload count of the promise
-    bytes32 public override payloadId;
+    /// @dev Deprecated. Kept solely to preserve storage layout (slot 50). Do not use.
+    uint40 private __requestCountDeprecated;

Then, append payloadId after existing vars (right before the gap) and update the doc:

@@
     /// @notice The revert handler data of the promise
     bytes public revertHandlerData;
 
-    // slots [55-102] reserved for gap
-    uint256[48] _gap_after;
+    /// @notice The payload id of the promise
+    bytes32 public override payloadId;
+
+    // slots [55-102] reserved for gap (reduced by 1 if you prefer strict accounting)
+    uint256[48] _gap_after;

No logic changes needed elsewhere; initialize() can continue setting payloadId. Also update the stale doc “payload count” → “payload id”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice The payload count of the promise
bytes32 public override payloadId;
/// @dev Deprecated. Kept solely to preserve storage layout (slot 50). Do not use.
uint40 private __requestCountDeprecated;
Suggested change
/// @notice The payload count of the promise
bytes32 public override payloadId;
/// @notice The revert handler data of the promise
bytes public revertHandlerData;
/// @notice The payload id of the promise
bytes32 public override payloadId;
// slots [55-102] reserved for gap (reduced by 1 if you prefer strict accounting)
uint256[48] _gap_after;
🤖 Prompt for AI Agents
In contracts/evmx/helpers/AsyncPromise.sol around lines 26 to 28, the change
replacing the packed uint40 with a bytes32 widens the storage slot and will
corrupt subsequent state; restore the original packed uint40 declaration (mark
it deprecated/unused to preserve the slot and name), keep localInvoker and all
following variables in their original packed layout, then add the new bytes32
payloadId field after the existing variables (immediately before the reserved
gap) with updated NatSpec ("payload id" not "payload count"); no other logic
changes are needed — initialize() may still set payloadId.

Comment on lines +32 to 37
function blockCredits(bytes32 payloadId_, address consumeFrom_, uint256 credits_) external;

function unblockAndAssignCredits(uint40 requestCount_, address assignTo_) external;
function unblockAndAssignCredits(bytes32 payloadId_, address assignTo_) external;

function unblockCredits(uint40 requestCount_) external;
function unblockCredits(bytes32 payloadId_) external;

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

🧩 Analysis chain

Breaking change: switch to payloadId (bytes32) for credit blocking

blockCredits/unblockAndAssignCredits/unblockCredits now use bytes32 payloadId_. All callers and events must align with the payload-centric flow.

This should include:

  • Updating all call sites.
  • Adjusting any storage keyed by requestCount to payloadId (mappings, events, logs).
  • Adding natspec for payloadId_ to avoid ambiguity.

Find usages still passing uint40:


🏁 Script executed:

#!/bin/bash
rg -nP --type=sol -C2 '\b(blockCredits|unblockAndAssignCredits|unblockCredits)\s*\('
rg -nP --type=sol -C2 '\bblockCredits\s*\(uint40\b|\bunblockAndAssignCredits\s*\(uint40\b|\bunblockCredits\s*\(uint40\b'

Length of output: 55


🏁 Script executed:

#!/bin/bash
rg -n -C2 '\bblockCredits\s*\(' -g '*.sol'
rg -n -C2 '\bunblockAndAssignCredits\s*\(' -g '*.sol'
rg -n -C2 '\bunblockCredits\s*\(' -g '*.sol'
rg -nP '\b(blockCredits|unblockAndAssignCredits|unblockCredits)\s*\(\s*uint40' -g '*.sol'

Length of output: 5140


Fix remaining call sites using uint40 requestCount

  • In deprecated/RequestHandler.sol (lines 131, 285), replace requestCount in blockCredits/unblockAndAssignCredits calls with the correct bytes32 payloadId (e.g. r.payloadId).
  • Add or update NatSpec on IFeesManager.sol for payloadId_ to clarify its purpose.
  • Audit any storage mappings or events previously keyed by uint40 requestCount to use bytes32 payloadId instead.
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IFeesManager.sol around lines 32 to 37, the
functions still reference payloadId_ but lack NatSpec and some call sites still
pass a uint40 requestCount; update deprecated/RequestHandler.sol at lines 131
and 285 to replace requestCount with the correct bytes32 payloadId (e.g.
r.payloadId) in blockCredits and unblockAndAssignCredits calls, add or update
NatSpec on payloadId_ in IFeesManager.sol to explain it is the unique payload
identifier (bytes32) used for credit operations, and audit/update any storage
mappings, event signatures, and indexing previously keyed by uint40 requestCount
to use bytes32 payloadId instead so all callsites and storage use the new
bytes32 key consistently.

Comment on lines +21 to +22
/// @notice The payload id of the promise
function payloadId() external view returns (bytes32);
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

🧩 Analysis chain

Breaking API change: verify all usages updated.

The migration from requestCount() returning uint40 to payloadId() returning bytes32 is a breaking change that aligns with the payload-centric model. This changes both the identifier type (sequential counter → content-based hash) and the semantic meaning.

Ensure all callers of the removed requestCount() have been updated throughout the codebase, particularly in:

  • Promise creation/tracking logic
  • Event emissions that reference request identifiers
  • Off-chain services or indexers that rely on this interface

Run this script to verify the migration is complete:


🏁 Script executed:

#!/bin/bash
# Verify that requestCount references have been removed and payloadId is used consistently

# Check for any remaining references to requestCount (excluding this interface change)
echo "=== Checking for remaining requestCount references ==="
rg -n 'requestCount' --type=solidity -g '!*.md' -g '!IPromise.sol'

# Verify payloadId usage in Promise implementations and related contracts
echo -e "\n=== Verifying payloadId implementation and usage ==="
ast-grep --pattern $'contract $_ {
  $$$
  function payloadId() $_ {
    $$$
  }
  $$$
}'

# Check for any hardcoded uint40 types that might be related to the old request counter
echo -e "\n=== Checking for uint40 usage that may need updating ==="
rg -n 'uint40' --type=solidity -C 2

Length of output: 232


🏁 Script executed:

#!/bin/bash
echo "=== Checking for remaining requestCount references ==="
rg -n 'requestCount' -g '*.sol' -g '!contracts/evmx/interfaces/IPromise.sol'

echo -e "\n=== Checking for uint40 usage ==="
rg -n 'uint40' -g '*.sol'

echo -e "\n=== Checking for payloadId references ==="
rg -n 'payloadId' -g '*.sol' -g '!contracts/evmx/interfaces/IPromise.sol'

Length of output: 62531


Breaking API change: migrate all requestCount usages to payloadId.

This should:

  • Replace uint40 requestCount parameters and returns with bytes32 payloadId in active contracts (e.g. contracts/evmx/helpers/AsyncDeployer.sol, contracts/evmx/interfaces/IAsyncDeployer.sol, contracts/evmx/base/AppGatewayBase.sol)
  • Remove all residual requestCount references outside deprecated/ tests
  • Update events, variables, and calls to use payloadId() instead of requestCount()
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IPromise.sol around lines 21-22, the interface
currently exposes payloadId() but the codebase still uses a deprecated uint40
requestCount API; update all active contracts to replace uint40 requestCount
parameters, returns, variables, events and calls with bytes32 payloadId (e.g.,
contracts/evmx/helpers/AsyncDeployer.sol,
contracts/evmx/interfaces/IAsyncDeployer.sol,
contracts/evmx/base/AppGatewayBase.sol), remove all non-test residual
requestCount references (leave deprecated/ tests untouched), change functions
signatures and return types to use bytes32 payloadId, update event definitions
and emitted values to pass payloadId(), and update any call sites to use
promise.payloadId() (or equivalent) instead of requestCount().

function isWatcher(address account_) external view returns (bool);
function increaseFees(bytes32 payloadId_, uint256 newMaxFees_) external;

function cancelRequest(bytes32 payloadId_) external;
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

🧩 Analysis chain

Verify all callers updated for bytes32 payloadId parameter.

The cancelRequest signature changed from uint40 requestCount to bytes32 payloadId. Ensure all calling code has been updated.


🏁 Script executed:

#!/bin/bash
# Find all cancelRequest invocations and verify they pass bytes32
rg -n -A2 'cancelRequest\s*\(' --type=sol --glob='!deprecated/**' | rg -v 'function cancelRequest'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for all cancelRequest calls in .sol files (excluding the interface definition) to verify updated parameter type
rg -n --glob '*.sol' 'cancelRequest\s*\(' --glob '!contracts/evmx/interfaces/IWatcher.sol' | rg -v 'function cancelRequest'

Length of output: 1254


Update cancelRequest callers to use bytes32 payloadId
Deprecated code and tests still call the old signature (cancelRequest(requestCount,…)):

  • deprecated/test/evmx/Watcher.t.sol: lines 364, 368, 374, 378, 394, 412
  • deprecated/Watcher.sol: 166
  • deprecated/RequestHandler.sol: 256, 266, 270
    Change these to pass the new bytes32 payloadId (or remove/replace deprecated code).
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IWatcher.sol around line 60, the cancelRequest
signature was changed to accept bytes32 payloadId_; update all callers to use
the new bytes32 payloadId parameter instead of the old (requestCount, …)
signature: edit deprecated/test/evmx/Watcher.t.sol (lines ~364, 368, 374, 378,
394, 412), deprecated/Watcher.sol (line 166), and deprecated/RequestHandler.sol
(lines ~256, 266, 270) to pass the bytes32 payloadId (or remove/replace
deprecated calls); ensure the caller obtains/constructs the correct payloadId
(bytes32) value and compiles against the updated interface.

Comment on lines +39 to 41
/// @notice Error thrown when payload count mismatch
error PayloadCountMismatch();

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Error rename requires repo-wide updates.

All usages of RequestCountMismatch must be replaced with PayloadCountMismatch to keep builds green.


🏁 Script executed:

#!/bin/bash
# Find old/new error references
rg -nP -C2 '\bRequestCountMismatch\b|\bPayloadCountMismatch\b'

Length of output: 1616


Update documentation for renamed error: Replace RequestCountMismatch() with PayloadCountMismatch() in Errors.md (line 148).

🤖 Prompt for AI Agents
In contracts/utils/common/Errors.sol around lines 39 to 41 the error was renamed
to PayloadCountMismatch(); update the documentation to reflect this rename by
replacing any instances of RequestCountMismatch() with PayloadCountMismatch() in
Errors.md (specifically line 148) and scan the docs for other occurrences to
update for consistency.


import "./ProxyStorage.t.sol";
import "../mock/MockWatcherPrecompile.sol";
// import "../mock/MockWatcherPrecompile.sol";
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

Compilation error: MockWatcherPrecompile still referenced.

Commenting out this import breaks compilation. The testUpgradeWithInitializationData function (lines 113-130) still instantiates MockWatcherPrecompile and uses its initialize selector.

Either restore the import or remove/update the test function that depends on it:

-// import "../mock/MockWatcherPrecompile.sol";
+import "../mock/MockWatcherPrecompile.sol";

Or if migrating away from MockWatcherPrecompile, remove the dependent test:

-    function testUpgradeWithInitializationData() public {
-        // Deploy new implementation
-        MockWatcherPrecompile newImpl = new MockWatcherPrecompile();
-
-        // Prepare initialization data with new defaultLimit
-        uint256 newValue = 2000;
-        bytes memory initData = abi.encodeWithSelector(
-            MockWatcherPrecompile.initialize.selector,
-            newValue
-        );
-
-        upgradeAndCall(address(watcher), address(newImpl), initData);
-        assertWatcherSlot();
-
-        // Verify new value is set
-        bytes32 slotValue = vm.load(address(watcher), bytes32(uint256(160)));
-        assertEq(uint256(slotValue), newValue, "newValue mismatch");
-    }
🤖 Prompt for AI Agents
In deprecated/test/evmx/ProxyMigration.t.sol around line 5 (and tests at
113-130), the commented out import for MockWatcherPrecompile breaks compilation
because testUpgradeWithInitializationData still instantiates and calls the
initialize selector on MockWatcherPrecompile; either restore the import line so
the contract is available, or modify the test: remove the MockWatcherPrecompile
instantiation and any calls to its initialize selector, or replace them with the
new mock/contract that provides the same initialize selector and behavior used
by the test; update any type names and selector usages accordingly and run the
test compile to ensure no remaining references to MockWatcherPrecompile exist.

Comment on lines +262 to 272
RawPayload[] memory RawPayload = new RawPayload[](0);

hoax(watcherAddress);
requestHandler.submitRequest(
1000,
address(0x1),
address(0x2),
address(0x3),
queueParams,
RawPayload,
""
);
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

🧩 Analysis chain

Avoid naming variables the same as types; fix zero-length RawPayload usage

Using RawPayload as a variable name collides with the type and hurts readability. Also, passing a zero-length payload array may revert depending on validation.

This should:

  • Rename the variable to lower camelCase (e.g., rawPayloads).
  • Confirm the API expects an array vs a single RawPayload. If array, ensure it’s non-empty with a minimally valid payload; if single, switch the declaration accordingly.
-        RawPayload[] memory RawPayload = new RawPayload[](0);
+        RawPayload[] memory rawPayloads = new RawPayload[](0);
...
-            RawPayload,
+            rawPayloads,
...
-            RawPayload,
+            rawPayloads,

Ensure the called submitRequest signature matches (array vs single RawPayload) in contracts/evmx/interfaces/IRequestHandler.sol. If it’s single, declare RawPayload memory rawPayload instead. Run:

Also applies to: 270-272, 281-283


🏁 Script executed:

#!/bin/bash
rg -nP -C2 --type=sol '\binterface\s+IRequestHandler\b' && rg -nP -C2 --type=sol '\bsubmitRequest\s*\('

Length of output: 27


Searching for IRequestHandler interface and submitRequest signature in .sol files:


🏁 Script executed:

#!/bin/bash
rg -n 'interface IRequestHandler' -C2 -g '*.sol'
rg -n 'function submitRequest' -C2 -g '*.sol'

Length of output: 1441


🏁 Script executed:

#!/bin/bash
# Locate RawPayload struct definition
rg -n 'struct RawPayload' -C3 -g '*.sol'

# Inspect Watcher.t.sol imports and variable declarations
sed -n '1,100p' deprecated/test/evmx/Watcher.t.sol
sed -n '250,300p' deprecated/test/evmx/Watcher.t.sol

# Verify submitRequest signature in interface and implementation
rg -n -C5 -g 'deprecated/IRequestHandler.sol' 'function submitRequest'
rg -n -C5 -g 'deprecated/RequestHandler.sol' 'function submitRequest'

Length of output: 7580


Fix submitRequest parameter type and variable naming

  • The submitRequest API expects a single RawPayload calldata, not an array. Replace
    RawPayload[] memory RawPayload = new RawPayload[](0);
    with
    RawPayload memory rawPayload = /* construct valid RawPayload struct */;
  • Rename the variable to rawPayload and update both calls:
    - requestHandler.submitRequest(..., RawPayload, "");
    + requestHandler.submitRequest(..., rawPayload, "");
🤖 Prompt for AI Agents
In deprecated/test/evmx/Watcher.t.sol around lines 262 to 272, the test
currently constructs an array of RawPayload and passes it to submitRequest, but
submitRequest expects a single RawPayload calldata; replace the array creation
with a single RawPayload memory variable named rawPayload (constructed with the
appropriate field values for your test), rename usages from RawPayload to
rawPayload, and pass rawPayload (not an array) to requestHandler.submitRequest
so the parameter types match.

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: 6

♻️ Duplicate comments (1)
contracts/evmx/helpers/AsyncPromise.sol (1)

26-27: CRITICAL: Storage layout break - bytes32 widening corrupts packed slot.

Replacing the packed uint40 with bytes32 at line 27 forces payloadId into a new slot, shifting localInvoker (line 31) and all subsequent storage variables. This will corrupt state on any upgrade or clone expecting the original layout.

This was flagged in previous reviews. Verify the fix was properly applied:

  1. Restore the original packed uint40 in slot 50 (mark it deprecated):
+    /// @dev Deprecated. Kept to preserve storage layout in slot 50. Do not use.
+    uint40 private __requestCountDeprecated;
-    /// @notice The payload count of the promise
-    bytes32 public override payloadId;
  1. Add payloadId after existing variables (before the gap):
     /// @notice The revert handler data of the promise
     bytes public revertHandlerData;
 
+    /// @notice The payload id of the promise
+    bytes32 public override payloadId;
+
     // slots [55-102] reserved for gap
     uint256[48] _gap_after;

The initialize() function at line 86 can continue setting payloadId without changes.

🧹 Nitpick comments (1)
contracts/evmx/helpers/AsyncPromise.sol (1)

181-185: LGTM: Validation logic correctly uses payload ID.

The function properly validates the caller and checks that the current promise matches the watcher's payload ID, preventing misuse.

Note: The error PayloadCountMismatch (line 184) is now semantically misleading since you're comparing payloadId (bytes32) not counts. Consider renaming to PayloadIdMismatch in a follow-up if it won't break error handling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1de79fa and 387b0dd.

📒 Files selected for processing (4)
  • contracts/evmx/base/AppGatewayBase.sol (7 hunks)
  • contracts/evmx/helpers/AsyncPromise.sol (4 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Watcher.sol (1 hunks)
🔇 Additional comments (19)
contracts/evmx/watcher/Watcher.sol (10)

67-115: LGTM: Payload execution flow is correct.

The fee handling sequence properly blocks credits upfront with maxFees, validates the actual fees returned by the precompile don't exceed the limit, and stores the payload with consistent state. The currentPayloadId is set before blocking credits and deploying the promise, ensuring proper tracking throughout the execution flow.


117-133: LGTM: Payload resolution logic is sound.

The function properly guards against double-execution, marks the payload state, invokes the promise resolution and precompile hooks, and settles fees. The flow is clear and maintains consistency.


135-152: LGTM: Promise resolution logic handles both success and failure paths.

The function correctly validates the deadline, attempts to mark the promise as resolved, and emits appropriate events for both success and failure cases. The early return on failure is appropriate.


154-176: LGTM: Revert handling is correctly implemented.

The function properly enforces that the deadline must have passed before allowing an onchain revert, cancels the execution, and conditionally marks the promise as reverting. The flow is logical and safe.


178-204: LGTM: Trigger flow is well-structured.

The function correctly checks for duplicate triggers, validates the plug, transfers fees, attempts the app gateway call with tryCall, and emits appropriate success/failure events. The state cleanup at the end is proper.

Note: This depends on the missing imports flagged earlier being added.


206-226: LGTM: Fee increase logic is correctly implemented.

The function properly validates the caller, checks payload state, unblocks the old fees, validates new fees are spendable, blocks the new amount, and emits the event. The guard conditions prevent misuse.


243-249: LGTM: PayloadId generation is consistent.

The function correctly retrieves the switchboard ID from the mapping and uses it with the payload count and evmxSlug to create a unique payload identifier. The logic is straightforward.

Verify that Configurations (the parent contract) exposes switchboards as a public mapping with signature mapping(uint32 => mapping(bytes32 => uint64)).


251-254: LGTM: Simple and correct getter.

The function properly exposes payload data for external consumers.


256-259: Verify setTriggerFees access control.

Line 256 uses onlyWatcher modifier. Since this sets a configuration parameter, verify this is the intended access control rather than onlyOwner. Typically, configuration settings are owner-controlled while execution functions are watcher-controlled.


266-271: LGTM: Fee query is properly delegated.

The function correctly forwards the query to the appropriate precompile.

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

76-88: LGTM: Initialize signature updated for payload model.

The function correctly accepts and stores payloadId_ (bytes32) instead of the old requestCount_ (uint40), aligning with the payload-centric architecture. The logic is sound.

Note: This depends on the storage layout issue being fixed first.


90-121: LGTM: Promise resolution logic is unchanged and correct.

The function properly guards against re-resolution, handles callback execution with tryCall, and transitions to the appropriate state on failure. The flow is sound.

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

10-20: LGTM: Imports align with payload-centric model.

The updated imports properly support the new RawPayload, OverrideParams, and OverrideParamsLib usage throughout the contract.


99-109: LGTM: Transaction control functions updated for payload model.

Both _revertTx and _increaseFees correctly use bytes32 payloadId_ and properly delegate to the watcher. The changes align with the payload-centric architecture.


137-160: LGTM: Helper functions correctly adapted to payload model.

The functions properly use overrideParams for configuration and delegate appropriately to the watcher. The logic is sound.


199-215: LGTM: Override management is cleaner and more maintainable.

The refactored override handling using OverrideParamsLib is clearer and more composable than the previous approach. The clear() method preserves the switchboard type while resetting other params, which is correct.

contracts/evmx/interfaces/IWatcher.sol (3)

4-8: LGTM: Interface properly declares dependencies.

The interface correctly imports the necessary types and extends IConfigurations, establishing the proper contract hierarchy.


9-20: LGTM: Events properly cover the payload lifecycle.

The events are well-designed with appropriate indexed parameters for efficient filtering and cover all key state transitions in the payload execution flow.


22-73: LGTM: Function signatures are well-defined and consistent.

The interface properly declares all public functions with appropriate parameter types and return values (except for the getCurrentPayloadId type mismatch flagged separately). The API surface is clear and complete for the payload-centric execution model.

Comment on lines 67 to 80
function _preAsync() internal {
isAsyncModifierSet = true;
_clearOverrides();
watcher__().clearQueue();
// watcher__().clearQueue();

isAsyncModifierSet = true;
currentPayloadId = _getCurrentPayloadId();
}

function _postAsync() internal {
isAsyncModifierSet = false;

(, address[] memory promises) = watcher__().submitRequest(
maxFees,
auctionManager,
consumeFrom,
onCompleteData
);
_markValidPromises(promises);
_clearOverrides();
// todo: get promise and mark it valid
// address promise_ = watcher__().latestAsyncPromise();
// isValidPromise[promise_] = 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 | 🟠 Major

Complete promise handling in _postAsync.

Lines 77-79 show TODOs for getting the promise and marking it valid. This logic is essential for the async flow to work correctly - without it, promises won't be validated and callbacks may fail.

The commented code suggests:

address promise_ = watcher__().latestAsyncPromise();
isValidPromise[promise_] = true;

Do you want me to generate the complete implementation or open an issue to track this?

🤖 Prompt for AI Agents
In contracts/evmx/base/AppGatewayBase.sol around lines 67 to 80, complete the
TODO in _postAsync by retrieving the latest async promise from the watcher and
marking it valid: call watcher__().latestAsyncPromise(), store the returned
address in a local variable, set isValidPromise[promise_] = true, and guard
against zero-address (e.g., require(promise_ != address(0)) or return) to avoid
marking an invalid promise; update any related state/events if the project
convention requires emitting a validation event.

Comment on lines 238 to 241
function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()));
emit PayloadSettled(payloadId_);
}
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

Critical: feesUsed_ parameter is ignored.

The function accepts feesUsed_ but doesn't pass it to unblockAndAssignCredits. This means actual fees used are not being accounted for - the FeesManager won't know how much to refund vs. charge.

Apply this diff to pass the fees through:

 function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
-    feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()));
+    feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()), feesUsed_);
     emit PayloadSettled(payloadId_);
 }

Note: Verify the unblockAndAssignCredits signature accepts a third parameter for fees.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()));
emit PayloadSettled(payloadId_);
}
function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()), feesUsed_);
emit PayloadSettled(payloadId_);
}
🤖 Prompt for AI Agents
In contracts/evmx/watcher/Watcher.sol around lines 238 to 241, the function
_settlePayload accepts feesUsed_ but does not pass it into
feesManager__().unblockAndAssignCredits, so fees aren't communicated; update the
call to pass feesUsed_ as the third argument (e.g.,
unblockAndAssignCredits(payloadId_, address(feesManager__()), feesUsed_)),
ensuring the FeesManager function signature supports a third fees parameter and
updating its interface/implementation if necessary.

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: 6

Caution

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

⚠️ Outside diff range comments (6)
contracts/evmx/fees/Credit.sol (2)

140-147: Fix double-mint on native-withdraw failure

On withdraw failure you mint creditAmount_ again and then add 0 to credits. This should mint nativeAmount_ as credits and update the tallies accordingly.

-            if (!success) {
-                _mint(depositTo_, creditAmount_);
-                nativeAmount_ = 0;
-                creditAmount_ += nativeAmount_;
-            }
+            if (!success) {
+                // Convert failed native transfer to credits instead of double-minting credits
+                _mint(depositTo_, nativeAmount_);
+                creditAmount_ += nativeAmount_;
+                nativeAmount_ = 0;
+            }

201-207: Allowance check must cover required amount

isCreditSpendable returns true when allowance is non-zero but still less than amount_, causing later revert in transferFrom. This should require allowance >= amount_.

-        if (spender_ != address(watcher__()) && consumeFrom_ != spender_) {
-            if (allowance(consumeFrom_, spender_) == 0) return false;
-        }
+        if (spender_ != address(watcher__()) && consumeFrom_ != spender_) {
+            if (allowance(consumeFrom_, spender_) < amount_) return false;
+        }
         return balanceOf(consumeFrom_) >= amount_;
test/SetupTest.t.sol (1)

703-706: Fix ABI decode type for precompileData

last tuple element must be uint64 (switchboard), not address. Wrong type will revert at decode even if unused.

-            (, Transaction memory transaction, , , , ) = abi.decode(
-                params.precompileData,
-                (address, Transaction, WriteFinality, uint256, uint256, address)
-            );
+            (, Transaction memory transaction, , , , ) = abi.decode(
+                params.precompileData,
+                (address, Transaction, WriteFinality, uint256, uint256, uint64)
+            );
contracts/evmx/helpers/AsyncDeployer.sol (1)

132-152: Potential issue: address prediction requires knowing asyncPromiseCounter.

The salt computation at line 151 includes asyncPromiseCounter, which means getAsyncPromiseAddress needs to know the counter value to predict the address. However, getAsyncPromiseAddress (lines 184-190) doesn't have access to what the counter value will be at deployment time.

This creates a race condition:

  • getAsyncPromiseAddress is called to predict an address
  • deployAsyncPromiseContract is called, incrementing the counter
  • The predicted address won't match the deployed address if other deployments happen in between

Consider either:

  1. Passing the counter value to getAsyncPromiseAddress
  2. Removing the counter from the salt and using only payloadId_ + invoker_ for uniqueness (if guaranteed unique)
  3. Returning the deployed address from deployAsyncPromiseContract and not relying on prediction

Run this script to check how getAsyncPromiseAddress is used:

#!/bin/bash
# Description: Check usage patterns of getAsyncPromiseAddress

echo "=== Finding getAsyncPromiseAddress usage ==="
rg -n -B5 -A5 'getAsyncPromiseAddress\s*\(' --type=sol

echo -e "\n=== Finding deployAsyncPromiseContract usage ==="
rg -n -B5 -A5 'deployAsyncPromiseContract\s*\(' --type=sol
contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

240-247: NatSpec says “Only callable by owner” but code enforces onlyWatcher.

Update the comment to match behavior or switch to onlyOwner. Given the rest of the file, keeping onlyWatcher is consistent; fix the doc.

-    /// @dev Only callable by the contract owner
+    /// @dev Only callable by the watcher
contracts/evmx/watcher/Configurations.sol (1)

145-155: Doc vs behavior: verifyConnections is unrestricted view.

NatSpec states “Only callable by the watcher” but the function is public view. Either add an access check in Watcher or adjust the doc here to avoid misleading readers.

♻️ Duplicate comments (6)
contracts/evmx/fees/Credit.sol (1)

269-280: Construct and apply override params before executePayload

overrideParams is never constructed; commented code leaves consumeFrom/maxFees unset, breaking fee accounting/execution. Build a local OverrideParams and assign it to rawPayload.overrideParams.

-        // applyOverride(
-        //     OverrideParamsLib.setMaxFees(overrideParams, getMaxFees(chainSlug_)).setConsumeFrom(consumeFrom_)
-        // );
-        RawPayload memory rawPayload;
-        rawPayload.overrideParams = overrideParams;
+        RawPayload memory rawPayload;
+        OverrideParams memory params;
+        params = OverrideParamsLib
+            .setMaxFees(params, getMaxFees(chainSlug_))
+            .setConsumeFrom(consumeFrom_);
+        rawPayload.overrideParams = params;
         rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
         watcher__().executePayload(rawPayload, address(this));

Add missing imports near the top:

+import {OverrideParams} from "../../utils/common/Structs.sol";
+import "../helpers/OverrideParamsLib.sol";
contracts/evmx/fees/FeesManager.sol (1)

58-59: Undefined overrideParams usage causes compilation failure and wrong data source for consumeFrom.

FeesManager doesn’t declare overrideParams and likely doesn’t inherit it. These references fail compilation and incorrectly fetch consumeFrom. This should read from the watcher payload for the given payloadId_. Also remove the stray overrideParams writes in initialize/setMaxFees (they don’t belong here and are redundant with maxFeesPerChainSlug).

Apply these diffs:

  1. Fix consumeFrom source in both unblocks:
-        // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
-        address consumeFrom = overrideParams.consumeFrom;
+        // derive payer from payload storage
+        address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
-        // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
-        address consumeFrom = overrideParams.consumeFrom;
+        // derive payer from payload storage
+        address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
  1. Drop invalid overrides in initialize and setMaxFees:
-        overrideParams = overrideParams.setSwitchboardType(sbType_).setMaxFees(fees_);
+        // sbType_ is not used by FeesManager; max fees tracked per-chain below.
-    function setMaxFees(uint256 fees_) external onlyOwner {
-        overrideParams = overrideParams.setMaxFees(fees_);
-    }
+    function setMaxFees(uint256 fees_) external onlyOwner {
+        // Keep per-chain cap aligned to evmxSlug’s default
+        maxFeesPerChainSlug[evmxSlug] = fees_;
+    }

If Credit.sol intentionally exposes overrideParams, state that explicitly and ensure it is initialized there; otherwise, keep the above fix.

Also applies to: 87-87, 120-121, 142-142

contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

150-190: gasLimit logic returns 0 when override is set.

_validate only assigns defaults when override is zero, but never initializes gasLimit from the override. Digest and precompile data will use 0 even when caller provided a positive value.

 function _validate(
     RawPayload calldata rawPayload_,
     address appGateway_
 ) internal view returns (uint256 gasLimit) {
+        // honor explicit override first
+        gasLimit = rawPayload_.overrideParams.gasLimit;
         if (
             rawPayload_.overrideParams.value >
             chainMaxMsgValueLimit[rawPayload_.transaction.chainSlug]
         ) revert MaxMsgValueLimitExceeded();

         if (
             rawPayload_.transaction.payload.length == 0 ||
             rawPayload_.transaction.payload.length > PAYLOAD_SIZE_LIMIT
         ) {
             revert InvalidPayloadSize();
         }

         if (rawPayload_.transaction.target == bytes32(0)) revert InvalidTarget();

         watcher__.verifyConnections(
             rawPayload_.transaction.chainSlug,
             rawPayload_.transaction.target,
             appGateway_,
             rawPayload_.overrideParams.switchboardType
         );

-        // todo: can be changed to set the default gas limit for each chain
-        if (rawPayload_.overrideParams.gasLimit == 0) {
+        // default per chain only when no override was provided
+        if (gasLimit == 0) {
             if (rawPayload_.transaction.chainSlug == 5000) {
                 // Mantle default gas limit
                 gasLimit = 8_000_000_000;
             } else if (rawPayload_.transaction.chainSlug == 1329) {
                 // Sei default gas limit
                 gasLimit = 8_000_000;
             } else if (rawPayload_.transaction.chainSlug == 999) {
                 // HyperEVM default gas limit
                 gasLimit = 1_500_000;
             } else {
                 gasLimit = 10_000_000; // other chains default gas limit
             }
         }
 }
contracts/evmx/interfaces/IWatcher.sol (1)

61-64: Critical: Type mismatch prevents compilation.

Line 63 declares uint32 switchboardType_ but the implementation at Watcher.sol:253 uses bytes32 switchboardType_. This mismatch will block compilation.

Apply this diff to fix the interface:

 function getCurrentPayloadId(
     uint32 chainSlug_,
-    uint32 switchboardType_
+    bytes32 switchboardType_
 ) external view returns (bytes32);
contracts/evmx/watcher/Watcher.sol (2)

4-13: Add missing imports for Converters and error definitions.

The contract references fromBytes32Format (line 188), AppGatewayAlreadyCalled (line 186), and InvalidCallerTriggered (line 190) but doesn't import them. This will cause compilation failure.

Add the missing imports:

 import "solady/utils/Initializable.sol";
 import "./Configurations.sol";
 import {IPrecompile} from "../interfaces/IPrecompile.sol";
 import {IFeesManager} from "../interfaces/IFeesManager.sol";
 import {IPromise} from "../interfaces/IPromise.sol";
 import {IERC20} from "../interfaces/IERC20.sol";
 import "../../utils/common/IdUtils.sol";
 import "solady/utils/LibCall.sol";
 import "solady/utils/ECDSA.sol";
+import {fromBytes32Format} from "../../utils/common/Converters.sol";

Also ensure the error definitions are available (either through inheritance or import):

  • AppGatewayAlreadyCalled
  • InvalidCallerTriggered
  • InsufficientFees
  • InvalidCallType
  • DeadlinePassed
  • DeadlineNotPassedForOnChainRevert
  • OnlyAppGateway
  • NewMaxFeesLowerThanCurrent
  • InvalidContract
  • InvalidData
  • InvalidSignature
  • NonceUsed

246-249: TODO: Complete settlement implementation.

The feesUsed_ parameter is accepted but never used, and the actual settlement call is commented out (line 247). This means credits remain blocked after cancellation.

Complete the implementation by uncommenting and passing the fees parameter:

-    // todo
     function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
-        // feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()));
+        feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__()), feesUsed_);
         emit PayloadSettled(payloadId_);
     }

Note: Verify that unblockAndAssignCredits accepts a third feesUsed_ parameter. If not, update the IFeesManager interface accordingly.

🧹 Nitpick comments (7)
test/SetupTest.t.sol (1)

721-722: Watcher-only call should impersonate watcher

markRevert likely requires onlyWatcher. This should hoax(watcherEOA) as done in resolvePayload/uploadProof.

-        watcher.markRevert(promiseReturnData, isRevertingOnchain_);
+        hoax(watcherEOA);
+        watcher.markRevert(promiseReturnData, isRevertingOnchain_);
contracts/evmx/interfaces/IConfigurations.sol (1)

46-50: Setter now binds appGateway to msg.sender; update docs and verify callsites

With appGateway_ removed, setIsValidPlug now implicitly uses msg.sender as the app gateway key while isValidPlug still accepts appGateway in reads. This should be documented in NatSpec and all writers should invoke from the correct address (or via proxy that preserves msg.sender).

test/apps/counter/Counter.sol (1)

20-22: Remove redundant getter function.

The getCounter() function is unnecessary since counter is already public and Solidity auto-generates a getter. This adds code bloat without value.

Apply this diff:

-    function getCounter() external view returns (uint256) {
-        return counter;
-    }
-
contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (1)

117-145: Parameter name should match IPrecompile interface definition.

The interface defines this parameter as address appGateway (IPrecompile.sol:21), but SchedulePrecompile uses an unnamed address parameter (line 119). While syntactically valid in Solidity, this violates consistency with the interface contract. WritePrecompile correctly names it appGateway, but ReadPrecompile has the same issue. This should be renamed to appGateway to match the interface definition, even though it remains unused in this implementation.

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

133-136: Doc mismatch: return type is bytes32, not uint40.

Fix the NatSpec to avoid confusion.

-    /// @return uint40 The current payload count
+    /// @return bytes32 The current payload id
contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

111-145: Event emits full RawPayload (gas-heavy).

Emitting the full struct inflates gas and log size. This should emit minimal fields (e.g., chainSlug, target, callType, gasLimit) and compute the rest off-chain.

test/apps/counter/CounterAppGateway.sol (1)

70-76: uploadPlug is wide open.

Anyone can set forwarder addresses in tests. Prefer onlyOwner to avoid flaky tests due to accidental calls.

-    function uploadPlug(uint32 chainSlug_, bytes32 contractId_, bytes32 plug_) public {
+    function uploadPlug(uint32 chainSlug_, bytes32 contractId_, bytes32 plug_) public onlyOwner {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 387b0dd and 6638193.

📒 Files selected for processing (18)
  • contracts/evmx/base/AppGatewayBase.sol (8 hunks)
  • contracts/evmx/fees/Credit.sol (5 hunks)
  • contracts/evmx/fees/FeesManager.sol (5 hunks)
  • contracts/evmx/helpers/AddressResolverUtil.sol (1 hunks)
  • contracts/evmx/helpers/AsyncDeployer.sol (3 hunks)
  • contracts/evmx/interfaces/IAsyncDeployer.sol (1 hunks)
  • contracts/evmx/interfaces/IConfigurations.sol (1 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Configurations.sol (4 hunks)
  • contracts/evmx/watcher/Watcher.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (5 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (4 hunks)
  • contracts/utils/OverrideParamsLib.sol (1 hunks)
  • test/SetupTest.t.sol (17 hunks)
  • test/apps/Counter.t.sol (4 hunks)
  • test/apps/counter/Counter.sol (1 hunks)
  • test/apps/counter/CounterAppGateway.sol (1 hunks)
  • test/apps/counter/ICounter.sol (1 hunks)
🔇 Additional comments (24)
contracts/evmx/fees/Credit.sol (1)

36-39: Key-type migration: bytes32 payloadId vs uint40

requestBlockedCredits now keys by bytes32. This should include a clear migration plan for any existing state and all callsites must pass bytes32 payloadId. Confirm no remaining uint40-based lookups exist.

test/SetupTest.t.sol (4)

147-158: Core-plug wiring helper looks good

setupGatewayAndPlugs composes AppGatewayConfig correctly and applies via watcher.setAppGatewayConfigs.


160-177: App gateway config composition LGTM

Config array sizing and field population are tight; hoax(watcherEOA) before set is correct.


228-230: Consistent watcher wiring

setSocket and setSwitchboard with FAST constant align with the new config model.


712-715: Resolve flow fee parameterization LGTM

resolvePayload with feesAmount under watcher hoax is consistent with the new flow.

contracts/evmx/helpers/AddressResolverUtil.sol (1)

25-32: Confirm intent: watcher owner access in AddressResolverUtil; fix stale NatSpec; cache watcher__() call

The broadened access in isWatcher() allowing watcher__().owner() conflicts with the modifier's NatSpec ("watcher precompile") and diverges from most other implementations (AddressResolver, ReadPrecompile, SchedulePrecompile all enforce strict precompile-only checks). Only WritePrecompile intentionally allows owner, suggesting that's an exception for write operations.

Either revert to strict msg.sender == address(watcher__()) or confirm owner bypass is intentional and update the NatSpec accordingly—currently contradictory docs ("precompile contract" vs "watcher owner"). Cache the watcher__() call to avoid two external reads.

contracts/evmx/helpers/AsyncDeployer.sol (1)

99-110: LGTM: deployAsyncPromiseContract correctly updated.

The function signature and implementation are consistent with the interface change. The asyncPromiseCounter increment happens after _createAsyncPromiseParams, which is correct since the counter is used in salt computation.

test/apps/counter/Counter.sol (1)

24-27: LGTM: Gateway interaction pattern is correct.

The function properly delegates to the gateway through the socket. The commented line about overrides is acceptable as documentation for future functionality.

test/apps/Counter.t.sol (4)

23-32: LGTM: deployCounter properly initializes the counter contract.

The deployment flow correctly:

  1. Creates the Counter instance
  2. Initializes the socket connection
  3. Sets up gateway and plug infrastructure
  4. Registers the plug with the gateway

45-59: LGTM: Test correctly validates counter increment via payload execution.

The test properly exercises the payload-centric flow: deploys counter, triggers increment through gateway, executes payload, and validates state change.


61-92: LGTM: Multi-chain test validates independent counter execution.

The test properly validates that counters on different chains can be incremented independently through the payload execution model.


113-124: LGTM: Schedule test validates delayed execution.

The test correctly validates that scheduled payloads are executed after the specified delay and within the expiry window.

contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2)

28-33: LGTM: Clean watcher governance pattern.

The watcher state variable and access modifier provide clear, explicit access control, replacing the previous inheritance-based approach.


147-152: LGTM: resolvePayload enforces schedule timing constraints.

The function correctly validates that the schedule delay has elapsed before allowing resolution. The error handling and event emission are appropriate.

contracts/evmx/interfaces/IAsyncDeployer.sol (1)

37-45: All call sites verified and correctly updated to bytes32.

Production code has been fully migrated:

  • Watcher.sol passes currentPayloadId (bytes32) to deployAsyncPromiseContract
  • AsyncDeployer.sol implementation correctly encodes bytes32 in constructor args and init data
  • Salt computation via keccak256(abi.encodePacked(constructorArgs, asyncPromiseCounter)) handles bytes32 without issue

Deprecated tests still use uint40, but that's immaterial. Implementation is sound.

contracts/utils/OverrideParamsLib.sol (2)

12-26: LGTM: sane defaults and fluent builder.

clear sets WRITE, LOW finality, zero fees, and accepts switchboardType. No issues.


116-134: LGTM: setter coverage is complete.

ConsumeFrom, maxFees, gas/value, parallel/read-at-block are covered. No functional concerns.

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

90-93: Schedule kick-off looks correct.

Setting SCHEDULE, delay, and executing a minimal RawPayload with overrides is consistent with the new flow.

contracts/evmx/watcher/Configurations.sol (1)

114-121: setIsValidPlug: scope is appropriate.

Binding to msg.sender ensures only the app gateway sets flags for itself. Event emission is correct.

test/apps/counter/CounterAppGateway.sol (1)

23-27: Constructor wiring looks correct.

Owner/init and maxFees override set via library; good for tests.

contracts/evmx/watcher/Watcher.sol (4)

85-97: Verify fee blocking strategy.

Lines 85-89 block maxFees before computing actual fees from the precompile (line 95-96). If the actual fees returned by the precompile is less than maxFees, the excess remains blocked until settlement. This ties up more credits than necessary.

Confirm this is intentional. If immediate adjustment is desired, unblock and re-block with actual fees:

         feesManager__().blockCredits(
             currentPayloadId,
             rawPayload_.overrideParams.consumeFrom,
             rawPayload_.overrideParams.maxFees
         );
         latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract(
             appGateway_,
             currentPayloadId
         );
         
         (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile(precompile)
             .handlePayload(rawPayload_, appGateway_, currentPayloadId);
         if (fees > rawPayload_.overrideParams.maxFees) revert InsufficientFees();
+        
+        // Adjust blocked credits to actual fees
+        if (fees < rawPayload_.overrideParams.maxFees) {
+            feesManager__().unblockCredits(currentPayloadId);
+            feesManager__().blockCredits(currentPayloadId, rawPayload_.overrideParams.consumeFrom, fees);
+        }

216-233: LGTM - Fee increase logic is sound.

The function correctly validates state (not cancelled, not executed, new fees higher), unblocks old credits, validates new credits are available, then reblocks with the new amount. The event emission enables off-chain actors to respond.


259-318: LGTM - Utility functions are well-implemented.

The getter/setter functions (getPayload, setTriggerFees, setPrecompile, getPrecompileFees) are straightforward and correct. The signature validation logic properly checks nonce reuse, constructs the digest with contract address and chain slug for replay protection, and uses ECDSA.recover with the EIP-191 prefix.


251-257: Disregard this review comment—the code is correct.

createPayloadId is consistently called across the codebase passing the destination chain as the third parameter (MessageSwitchboard.sol line 165, Socket.sol). The payload ID must be scoped to the destination chain since getCurrentPayloadId receives chainSlug_ as input and uses it to look up the switchboard configuration. Using evmxSlug would create ID collisions for different destination chains sharing the same payload counter.

mapping(address => bool) public isValidPromise;

// slot 58
mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses;
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

Invalid override on state variable.

public override forwarderAddresses is not allowed on state vars. This should compile as a plain public mapping.

-    mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses;
+    mapping(bytes32 => mapping(uint32 => address)) public forwarderAddresses;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses;
mapping(bytes32 => mapping(uint32 => address)) public forwarderAddresses;
🤖 Prompt for AI Agents
In contracts/evmx/base/AppGatewayBase.sol around line 36, the state variable
declaration uses an invalid `override` modifier on a public mapping; remove
`override` so it is declared as a plain public mapping (i.e., change
`mapping(...) public override forwarderAddresses;` to `mapping(...) public
forwarderAddresses;`) and ensure any required overriding is handled on
functions, not state variables.

Comment on lines +69 to 70
isAsyncModifierSet = 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 | 🟠 Major

Async guard not reset and missing zero-address check for promise.

isAsyncModifierSet is set true in _preAsync but never cleared, allowing calls like _setSchedule outside the intended async scope. Also guard against zero promise.

 function _postAsync() internal {
-        _clearOverrides();
-        address promise_ = watcher__().latestAsyncPromise();
-        isValidPromise[promise_] = true;
+        _clearOverrides();
+        address promise_ = watcher__().latestAsyncPromise();
+        if (promise_ != address(0)) {
+            isValidPromise[promise_] = true;
+        }
+        // close the async window
+        isAsyncModifierSet = false;
 }

Also applies to: 72-76

🤖 Prompt for AI Agents
In contracts/evmx/base/AppGatewayBase.sol around lines 69-70 (and similarly
lines 72-76), _preAsync sets isAsyncModifierSet true but nothing clears it and
_setSchedule lacks a zero-address check for the promise; fix by adding
isAsyncModifierSet = false in the corresponding _postAsync function so the guard
is cleared after async execution, and add a require(promise != address(0),
"invalid promise") at the start of _setSchedule to prevent zero-address
promises; ensure the modifier/flow guarantees _postAsync always runs (so the
flag is cleared on all execution paths).

Comment on lines 223 to 227
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();

if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
return super.transferFrom(from_, to_, amount_);
}
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

Don’t clobber user approvals when caller is watcher

Setting allowance to amount_ for watcher overwrites any existing allowance (including infinite). This should bypass allowance entirely for watcher and perform a direct transfer.

-        if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
-        return super.transferFrom(from_, to_, amount_);
+        if (msg.sender == address(watcher__())) {
+            _transfer(from_, to_, amount_);
+            return true;
+        }
+        return super.transferFrom(from_, to_, amount_);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
return super.transferFrom(from_, to_, amount_);
}
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
if (msg.sender == address(watcher__())) {
_transfer(from_, to_, amount_);
return true;
}
return super.transferFrom(from_, to_, amount_);
}
🤖 Prompt for AI Agents
In contracts/evmx/fees/Credit.sol around lines 223 to 227, the current logic
sets the watcher's allowance to amount_ (clobbering any existing approval)
before calling super.transferFrom; instead, detect when msg.sender ==
address(watcher__()) and bypass allowance handling entirely by performing a
direct internal transfer (call the contract's internal _transfer(from_, to_,
amount_) and return true) so existing user allowances (including infinite
approvals) are not overwritten; otherwise, keep the existing path that uses
super.transferFrom.

Comment on lines +10 to +13
import {DigestParams, Payload} from "../../../utils/common/Structs.sol";

import {WRITE, PAYLOAD_SIZE_LIMIT, CHAIN_SLUG_SOLANA_MAINNET, CHAIN_SLUG_SOLANA_DEVNET} from "../../../utils/common/Constants.sol";
import {InvalidIndex, MaxMsgValueLimitExceeded, InvalidPayloadSize} from "../../../utils/common/Errors.sol";
import {InvalidIndex, MaxMsgValueLimitExceeded, InvalidPayloadSize, OnlyWatcherAllowed, InvalidTarget} from "../../../utils/common/Errors.sol";
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

Missing RawPayload import and event type will not compile.

The file uses RawPayload in the event and function signatures but doesn’t import it. Add it to the Structs import.

-import {DigestParams, Payload} from "../../../utils/common/Structs.sol";
+import {DigestParams, Payload, RawPayload} from "../../../utils/common/Structs.sol";

Also applies to: 63-64, 98-107

🤖 Prompt for AI Agents
In contracts/evmx/watcher/precompiles/WritePrecompile.sol around lines 10-13
(and also referenced at lines 63-64 and 98-107), the RawPayload type is used but
not imported from Structs.sol causing compilation errors; update the import from
"../../../utils/common/Structs.sol" to include RawPayload (e.g., import
{DigestParams, Payload, RawPayload} ...) so the event and function signatures
that reference RawPayload compile correctly.

Comment on lines +98 to +106
function withdrawCredits(
uint32 chainSlug_,
address token_,
uint256 amount_,
address receiver_
) external {
_withdrawCredits(chainSlug_, token_, amount_, receiver_);
}

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

Unrestricted withdrawCredits lets anyone drain credits in tests.

Even for tests, this is risky and can mask real issues. Restrict to owner (or watcher) to reflect real-world usage.

-    function withdrawCredits(
+    function withdrawCredits(
         uint32 chainSlug_,
         address token_,
         uint256 amount_,
         address receiver_
-    ) external {
+    ) external onlyOwner {
         _withdrawCredits(chainSlug_, token_, amount_, receiver_);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function withdrawCredits(
uint32 chainSlug_,
address token_,
uint256 amount_,
address receiver_
) external {
_withdrawCredits(chainSlug_, token_, amount_, receiver_);
}
function withdrawCredits(
uint32 chainSlug_,
address token_,
uint256 amount_,
address receiver_
) external onlyOwner {
_withdrawCredits(chainSlug_, token_, amount_, receiver_);
}
🤖 Prompt for AI Agents
In test/apps/counter/CounterAppGateway.sol around lines 98 to 106, the
withdrawCredits function is publicly callable allowing anyone to drain credits;
restrict it to an authorized actor by adding an access control check (e.g.,
require(msg.sender == owner || msg.sender == watcher) or apply an
onlyOwner/onlyWatcher modifier). Update the contract to store and expose the
owner/watcher address (or inherit OpenZeppelin Ownable) and use that
check/modifier on withdrawCredits, and adjust tests to call withdrawCredits from
the authorized account.

Comment on lines +5 to +11
function increase() external;

function getCounter() external;

// A function that is not part of the interface, used for testing on-chian revert.
function wrongFunction() external;
}
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

Correct interface: add return type and view mutability

getCounter must declare view returns (uint256) to match implementations and avoid interface/override mismatch. Fix the typo in the comment.

-interface ICounter {
-    function increase() external;
-
-    function getCounter() external;
-
-    // A function that is not part of the interface, used for testing on-chian revert.
-    function wrongFunction() external;
-}
+interface ICounter {
+    function increase() external;
+    function getCounter() external view returns (uint256);
+    // A function that is not part of the interface, used for testing on-chain revert.
+    function wrongFunction() external;
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In test/apps/counter/ICounter.sol around lines 5 to 11, the interface is
incorrect: change the getCounter declaration to include view and a uint256
return type (i.e., declare it as view returns (uint256)) so it matches
implementations and overrides, and fix the comment typo "on-chian" to "on-chain"
to clarify the intent; leave or remove wrongFunction as desired for tests but
ensure interface signatures compile.

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: 12

Caution

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

⚠️ Outside diff range comments (8)
contracts/evmx/fees/Credit.sol (2)

138-149: Double‑mint bug on deposit when native transfer fails

on failure you mint creditAmount_ again and then add zero. This should mint nativeAmount_ as credits and adjust emitted values.

-        // Mint tokens to the user
-        _mint(depositTo_, creditAmount_);
+        // Mint credits
+        _mint(depositTo_, creditAmount_);
         if (nativeAmount_ > 0) {
             // if native transfer fails, add to credit
             bool success = feesPool.withdraw(depositTo_, nativeAmount_);
 
             if (!success) {
-                _mint(depositTo_, creditAmount_);
-                nativeAmount_ = 0;
-                creditAmount_ += nativeAmount_;
+                uint256 creditFallback = nativeAmount_;
+                _mint(depositTo_, creditFallback);
+                creditAmount_ += creditFallback;
+                nativeAmount_ = 0;
             }
         }
 
         emit Deposited(chainSlug_, token_, depositTo_, creditAmount_, nativeAmount_);

178-181: Guard against underflow in balanceOf

If userBlockedCredits exceeds total, this reverts. This should saturate to zero.

-    function balanceOf(address account) public view override returns (uint256) {
-        return super.balanceOf(account) - userBlockedCredits[account];
-    }
+    function balanceOf(address account) public view override returns (uint256) {
+        uint256 total = super.balanceOf(account);
+        uint256 blocked = userBlockedCredits[account];
+        return blocked > total ? 0 : total - blocked;
+    }
hardhat-scripts/deploy/3.configureChains.ts (2)

148-152: Bug: double toBytes32 conversion for socket.

You compute socket as bytes32, then convert it again before sending on-chain. This should pass bytes32 exactly once.

-  const socket = toBytes32FormatHexString(chainAddresses[Contracts.Socket]);
+  const socket = chainAddresses[Contracts.Socket];
...
-  [chain],
-  toBytes32FormatHexString(socket),
+  [chain],
+  toBytes32FormatHexString(socket),
...
-  [chain, toBytes32FormatHexString(socket)],
+  [chain, toBytes32FormatHexString(socket)],

Alternatively, keep socket as bytes32 and remove subsequent conversions:

-  const socket = toBytes32FormatHexString(chainAddresses[Contracts.Socket]);
+  const socket = toBytes32FormatHexString(chainAddresses[Contracts.Socket]);
...
-  toBytes32FormatHexString(socket),
+  socket,
...
-  [chain, toBytes32FormatHexString(socket)],
+  [chain, socket],

Also applies to: 175-184


186-201: Bug: double toBytes32 conversion for feesPlug.

Same issue: feesPlug is set as bytes32, then converted again in updateContractSettings. This should only be converted once.

-  const feesPlug = toBytes32FormatHexString(
-    chainAddresses[Contracts.FeesPlug]!
-  );
+  const feesPlug = chainAddresses[Contracts.FeesPlug]!;
...
-  [chain],
-  toBytes32FormatHexString(feesPlug).toString(),
+  [chain],
+  toBytes32FormatHexString(feesPlug),
...
-  [chain, toBytes32FormatHexString(feesPlug)],
+  [chain, toBytes32FormatHexString(feesPlug)],
contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (3)

62-69: Fix guard: comparing against stale state lets invalid configs slip through

Constructor checks maxScheduleDelayInSeconds against old expiryTime (0), not the new input. This should compare inputs before assignment.

Apply:

     maxScheduleDelayInSeconds = maxScheduleDelayInSeconds_;
     scheduleFeesPerSecond = scheduleFeesPerSecond_;
     scheduleCallbackFees = scheduleCallbackFees_;
-    if (maxScheduleDelayInSeconds < expiryTime) revert InvalidScheduleDelay();
-    expiryTime = expiryTime_;
+    if (maxScheduleDelayInSeconds_ < expiryTime_) revert InvalidScheduleDelay();
+    expiryTime = expiryTime_;
     watcher__ = IWatcher(watcher_);

80-84: Fix setter guards: validate new value against the existing counterpart

Both setters validate current values instead of the new input, allowing invalid updates.

Use:

 function setMaxScheduleDelayInSeconds(uint256 maxScheduleDelayInSeconds_) external onlyWatcher {
-    if (maxScheduleDelayInSeconds < expiryTime) revert InvalidScheduleDelay();
+    if (maxScheduleDelayInSeconds_ < expiryTime) revert InvalidScheduleDelay();
     maxScheduleDelayInSeconds = maxScheduleDelayInSeconds_;
     emit MaxScheduleDelayInSecondsSet(maxScheduleDelayInSeconds_);
 }

 function setExpiryTime(uint256 expiryTime_) external onlyWatcher {
-    if (maxScheduleDelayInSeconds < expiryTime) revert InvalidScheduleDelay();
+    if (maxScheduleDelayInSeconds < expiryTime_) revert InvalidScheduleDelay();
     expiryTime = expiryTime_;
     emit ExpiryTimeSet(expiryTime_);
 }

Also applies to: 108-112


71-74: Encode/decode mismatch: getPrecompileFees decodes 1 item but precompileData encodes 2

abi.decode expects the exact layout. You encode (delay, executeAfter) but decode only (delay). This should compute fees from the raw delay to avoid decoding mismatch.

Apply:

-        precompileData = abi.encode(rawPayload.overrideParams.delayInSeconds, executeAfter);
-        fees = getPrecompileFees(precompileData);
+        precompileData = abi.encode(rawPayload.overrideParams.delayInSeconds, executeAfter);
+        // compute directly from delay to avoid ABI layout dependency
+        fees = scheduleFeesPerSecond * rawPayload.overrideParams.delayInSeconds + scheduleCallbackFees;

Optionally update getPrecompileFees doc to clarify it expects abi.encode(delayInSeconds).

Also applies to: 126-131

test/SetupTest.t.sol (1)

711-716: Wrong decode type for Write precompileData

WritePrecompile encodes the last field as uint64 switchboardId, not address. This decode will revert.

Apply:

-            (, Transaction memory transaction, , , , ) = abi.decode(
-                params.precompileData,
-                (address, Transaction, WriteFinality, uint256, uint256, address)
-            );
+            (, Transaction memory transaction, , , , ) = abi.decode(
+                params.precompileData,
+                (address, Transaction, WriteFinality, uint256, uint256, uint64)
+            );
♻️ Duplicate comments (7)
contracts/evmx/base/AppGatewayBase.sol (2)

36-36: Invalid override modifier on state variable.

State variables cannot use the override keyword in Solidity. This will cause a compilation error. Remove the override modifier.

Apply this diff:

-    mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses;
+    mapping(bytes32 => mapping(uint32 => address)) public forwarderAddresses;

65-74: Critical: Async guard never reset and missing zero-address validation.

Two issues in the async flow:

  1. isAsyncModifierSet is set to true in _preAsync (line 67) but never reset to false in _postAsync. This leaves the async guard permanently open after the first async call, allowing functions like _setSchedule to be called outside their intended async scope.

  2. No validation that promise_ is non-zero before marking it valid (line 73). If executePayload() returns address(0), an invalid promise will be marked as valid.

Apply this diff:

 function _postAsync() internal {
     _clearOverrides();
     address promise_ = watcher__().executePayload();
-    isValidPromise[promise_] = true;
+    if (promise_ != address(0)) {
+        isValidPromise[promise_] = true;
+    }
+    isAsyncModifierSet = false;
 }
contracts/evmx/fees/Credit.sol (2)

71-72: Missing imports for OverrideParams types and library

using OverrideParamsLib requires importing both the type and the library. RawPayload/Transaction are used later.

 import {toBytes32Format} from "../../utils/common/Converters.sol";
+import {OverrideParams, RawPayload, Transaction} from "../../utils/common/Structs.sol";
+import "../../utils/OverrideParamsLib.sol";

226-228: Do not clobber approvals when caller is watcher

Approving amount_ overwrites existing approvals (incl. infinite) and is unnecessary. Bypass allowance and transfer directly when watcher calls.

-        if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
-        return super.transferFrom(from_, to_, amount_);
+        if (msg.sender == address(watcher__())) {
+            _transfer(from_, to_, amount_);
+            return true;
+        }
+        return super.transferFrom(from_, to_, amount_);
contracts/evmx/fees/FeesManager.sol (1)

139-146: Fix: wrong source for consumeFrom in unblockCredits (also duplicates a prior issue).

This should read consumeFrom from the payload, not from overrideParams. Using overrideParams.consumeFrom will compile-break if the var isn’t declared here and is semantically wrong (it’s not per‑payload). Replace with watcher payload read.

-        // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom;
-        address consumeFrom = overrideParams.consumeFrom;
+        // consumeFrom is stored per-payload by the watcher
+        address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
contracts/evmx/watcher/Watcher.sol (1)

196-222: Add missing imports to Watcher.sol

The function _callAppGateways uses fromBytes32Format and custom errors that aren't imported. Solidity doesn't inherit imports through contract inheritance—Watcher.sol needs direct imports even though Configurations already has them.

Add these imports after line 11:

 import "../../utils/common/IdUtils.sol";
 import "solady/utils/LibCall.sol";
+import {fromBytes32Format} from "../../utils/common/Converters.sol";
+import "../../utils/common/Errors.sol";

Code won't compile without these.

contracts/evmx/interfaces/IWatcher.sol (1)

65-68: Critical: Type mismatch still not fixed.

Line 67 declares uint32 switchboardType_ but the implementation uses bytes32. This was flagged in previous review and remains unfixed. This will cause compilation failure.

Apply this diff:

 function getCurrentPayloadId(
     uint32 chainSlug_,
-    uint32 switchboardType_
+    bytes32 switchboardType_
 ) external view returns (bytes32);
🧹 Nitpick comments (12)
contracts/evmx/helpers/AsyncDeployer.sol (1)

99-102: Consider adding input validation for payloadId_.

The function doesn't validate that payloadId_ is non-zero. An empty payload ID might indicate an error in the calling code.

Add validation:

 function deployAsyncPromiseContract(
     address invoker_,
     bytes32 payloadId_
 ) external override onlyWatcher returns (address newAsyncPromise) {
+    require(payloadId_ != bytes32(0), "Invalid payloadId");
     // creates init data and salt
hardhat-scripts/deploy/3.configureChains.ts (1)

203-212: If re‑enabled: fix contractFactory double conversion before using.

The commented block also converts bytes32 twice. This should mirror the fix above.

-  const contractFactory = toBytes32FormatHexString(
-    chainAddresses[Contracts.ContractFactoryPlug]
-  );
+  const contractFactory = chainAddresses[Contracts.ContractFactoryPlug];
...
-  toBytes32FormatHexString(contractFactory).toString(),
+  toBytes32FormatHexString(contractFactory),
...
-  [chain, toBytes32FormatHexString(contractFactory)],
+  [chain, toBytes32FormatHexString(contractFactory)],
src/enums.ts (1)

49-51: Remove commented enum members to avoid drift.

Dead, commented constants should be deleted to keep the public surface clean. Git tracks history.

-  // AuctionEnded = "AuctionEnded",
-  // AuctionRestarted = "AuctionRestarted",
src/events.ts (1)

38-41: Delete the commented export block.

Keep the module lean; commented exports add noise.

-// export const auctionManagerEvents = [
-//   Events.AuctionEnded,
-//   Events.AuctionRestarted,
-// ];
contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2)

12-15: Docs are wrong: this is a contract, not a library with pure functions

Update the title/notice/dev comments to match the implementation.


80-112: Access-control docs mismatch

NatSpec says “Only callable by the contract owner” but these functions are onlyWatcher-gated. Update the comments.

test/SetupTest.t.sol (1)

503-506: Event signature drift from SchedulePrecompile

SchedulePrecompile emits ScheduleRequested(payloadId, executeAfter, deadline, localInvoker, selector, data). The test’s local event has only (payloadId, deadline). If you plan to expect/parse ScheduleRequested, this should mirror the contract signature.

contracts/evmx/watcher/precompiles/ReadPrecompile.sol (2)

25-33: Access control consistency across precompiles

Read/Schedule restrict to watcher address; Write allows watcher OR watcher.owner(). This should be consistent to avoid surprising governance paths. Pick one model repo‑wide.


78-81: Docs mismatch: “Only owner” vs onlyWatcher

Fix NatSpec to reflect onlyWatcher authorization.

contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)

68-73: Access control inconsistency with other precompiles

WritePrecompile allows watcher OR watcher.owner(). Read/Schedule restrict to watcher. This should be consistent.

Option A: Restrict to address(watcher__) only. Option B: Allow owner in all three. Document the model.


60-61: Event payload size

Emitting full RawPayload can bloat logs. If not strictly needed on-chain, emit compact fields (payloadId, digest, chainSlug, gasLimit) to cut gas.

contracts/evmx/watcher/Configurations.sol (1)

163-181: Clarify contractAddress_ parameter purpose.

The digest includes both address(this) and contractAddress_ (line 176), but when called at line 79, contractAddress_ is also address(this). This means address(this) appears twice in the digest. Is this intentional? If contractAddress_ is meant to allow validation for different contracts, consider whether including it twice adds security value or just creates confusion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6638193 and 9aae799.

📒 Files selected for processing (31)
  • FunctionSignatures.md (1 hunks)
  • contracts/evmx/base/AppGatewayBase.sol (7 hunks)
  • contracts/evmx/fees/Credit.sol (6 hunks)
  • contracts/evmx/fees/FeesManager.sol (4 hunks)
  • contracts/evmx/helpers/AddressResolver.sol (0 hunks)
  • contracts/evmx/helpers/AsyncDeployer.sol (3 hunks)
  • contracts/evmx/helpers/Forwarder.sol (2 hunks)
  • contracts/evmx/interfaces/IConfigurations.sol (2 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Configurations.sol (6 hunks)
  • contracts/evmx/watcher/Watcher.sol (2 hunks)
  • contracts/evmx/watcher/precompiles/ReadPrecompile.sol (2 hunks)
  • contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (5 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (4 hunks)
  • hardhat-scripts/deploy/1.deploy.ts (3 hunks)
  • hardhat-scripts/deploy/2.roles.ts (1 hunks)
  • hardhat-scripts/deploy/3.configureChains.ts (1 hunks)
  • hardhat-scripts/deploy/4.configureEVMx.ts (3 hunks)
  • hardhat-scripts/deploy/6.connect.ts (1 hunks)
  • hardhat-scripts/deploy/8.setupEnv.ts (0 hunks)
  • hardhat-scripts/deploy/9.setupTransmitter.ts (0 hunks)
  • hardhat-scripts/deploy/UpgradeForwarder.ts (1 hunks)
  • hardhat-scripts/deploy/UpgradePromise.ts (1 hunks)
  • hardhat-scripts/deploy/WhitelistFeesReceiver.ts (0 hunks)
  • package.json (1 hunks)
  • src/enums.ts (2 hunks)
  • src/events.ts (2 hunks)
  • src/signer.ts (1 hunks)
  • src/types.ts (0 hunks)
  • test/SetupTest.t.sol (17 hunks)
  • test/apps/counter/CounterAppGateway.sol (1 hunks)
💤 Files with no reviewable changes (5)
  • hardhat-scripts/deploy/WhitelistFeesReceiver.ts
  • src/types.ts
  • hardhat-scripts/deploy/8.setupEnv.ts
  • hardhat-scripts/deploy/9.setupTransmitter.ts
  • contracts/evmx/helpers/AddressResolver.sol
✅ Files skipped from review due to trivial changes (3)
  • src/signer.ts
  • package.json
  • hardhat-scripts/deploy/UpgradePromise.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • contracts/evmx/helpers/Forwarder.sol
  • test/apps/counter/CounterAppGateway.sol
  • FunctionSignatures.md
🧰 Additional context used
🧬 Code graph analysis (1)
hardhat-scripts/deploy/1.deploy.ts (1)
hardhat-scripts/config/config.ts (2)
  • EVMX_CHAIN_ID (181-181)
  • transmitter (183-183)
🔇 Additional comments (27)
contracts/evmx/base/AppGatewayBase.sol (8)

83-91: LGTM! Schedule logic is correct.

The callType is now correctly set to SCHEDULE (line 85), and the payload is properly constructed with override parameters before being added to the watcher.


93-103: LGTM! Payload-based admin helpers are correct.

The refactored _revertTx and _increaseFees functions correctly use payloadId_ with the new watcher APIs (cancelExecution and increaseFees).


143-145: LGTM! Switchboard type setter is correct.

The function correctly updates overrideParams.switchboardType.


157-167: LGTM! Permit helper is correct.

The function properly decodes the approval data and forwards it to the token's permit function.


174-188: LGTM! Withdraw credits uses correct override params.

The function correctly uses overrideParams.maxFees for the fee calculation in the new payload-based model.


194-213: LGTM! Override parameter helpers are correct.

The getter, clear, and apply functions properly manage the OverrideParams state using OverrideParamsLib.


109-136: LGTM! Utility functions are correct.

The getOnChainAddress, _createContractId, and _getCurrentPayloadId functions correctly implement address resolution and payload ID retrieval in the new payload-based model.


222-222: LGTM! Virtual hook is correct.

The handleRevert hook provides the expected extension point for derived contracts.

contracts/evmx/helpers/AsyncDeployer.sol (3)

180-186: Implementation is consistent with deployment flow.

The address prediction correctly mirrors the salt generation logic in _createAsyncPromiseParams. However, this prediction is only reliable if asyncPromiseCounter doesn't change between prediction and deployment (see comment on lines 99-110).


132-148: ****

The AsyncPromise.initialize signature already matches the parameter order in AsyncDeployer._createAsyncPromiseParams. Both encode and invoke parameters as (payloadId_, invoker_, addressResolver_). The AsyncPromise constructor is intentionally empty (disables initializers for the implementation), and the encoding parameters are used for salt calculation, not constructor invocation. No changes needed.

Likely an incorrect or invalid review comment.


99-110: Address prediction function has race condition but is currently unused.

The race condition in the review comment is technically valid: getAsyncPromiseAddress reads the current asyncPromiseCounter to predict an address, but the counter can increment between prediction and deployment, making the predicted address stale.

However, this function is exposed in the IAsyncDeployer interface but not called in production codedeployAsyncPromiseContract in Watcher.sol deploys directly without pre-prediction. The counter is intentionally designed to support multiple AsyncPromise contracts per (invoker_, payloadId_) pair.

The practical risk depends on intended usage:

  • If external integrations will use getAsyncPromiseAddress for off-chain address prediction, this race condition is a bug that needs fixing (salt should use only payloadId_ if it's guaranteed unique per deployment, or remove the function entirely).
  • If getAsyncPromiseAddress is meant only for post-deployment verification or won't be used, the counter design is fine as-is.

Clarify the intended use case and either remove the unused function or document that it's unsafe for cross-process address prediction.

hardhat-scripts/deploy/1.deploy.ts (1)

304-313: ContractFactoryPlug deployment removed

Good cleanup aligned with plugs list. No further action.

contracts/evmx/fees/Credit.sol (1)

36-39: Storage key change for blocked credits requires migration plan

Switching mapping key from uint40 requestCount to bytes32 payloadId changes the storage hashing. Existing entries (if any) become unreachable. This should include an upgrade/migration (or explicit reset) plan to prevent stranded balances.

Explain whether this contract is freshly deployed or provide a migration that rekeys old entries to payloadId.

hardhat-scripts/deploy/UpgradeForwarder.ts (1)

4-4: Import verified—ChainSlug is correctly re-exported

ChainSlug is defined in src/chain-enums/chainSlug.ts and re-exported through src/index.ts via the export * from "./chain-enums" barrel export. The import from "../../src" will resolve correctly with no type or runtime errors.

hardhat-scripts/deploy/6.connect.ts (1)

18-20: This change is incomplete—ContractFactoryPlug still actively used elsewhere in codebase

The plugs array narrowing achieves its immediate goal in this file (no init/connectSocket calls to ContractFactoryPlug), but leaves the system inconsistent:

  • hardhat-scripts/utils/gatewayId.ts:12 — Active case handler for ContractFactoryPlug
  • hardhat-scripts/deploy/3.configureChains.ts:150 — Reads ContractFactoryPlug from chainAddresses
  • hardhat-scripts/admin/rescue.ts:79 — Expects ContractFactoryPlug in address array
  • Related deployment & config code commented out in 1.deploy.ts, 2.roles.ts, 3.configureChains.ts

This suggests either:

  1. Incomplete refactoring — Must clean up all ContractFactoryPlug references system-wide, or
  2. Partial deprecation — Needs clear decision: keep it available or remove it entirely

Environment-gating is not a solution here. If ContractFactoryPlug removal is intentional, gate or remove the references throughout. If accidental, complete the refactoring.

contracts/evmx/fees/FeesManager.sol (2)

119-133: Good: per‑payload consumeFrom source and clean accounting.

Reading consumeFrom from watcher payload and using the cached blockedCredits_ eliminates mismatch risks. Event payload is correct.


58-59: State variable overrideParams is properly declared and inherited.

The variable exists in AppGatewayBase.sol (line 27) as a public state variable of type OverrideParams. FeesManager inherits it through the chain: FeesManager → Credit → AppGatewayBase. Both assignments at lines 58-59 and 86-88 are valid, as is the access at line 141. No compilation errors.

src/events.ts (1)

12-24: watcherEvents consolidation verified—removed arrays have no external references.

The codebase contains no imports of promiseResolverEvents, requestHandlerEvents, or configurationsEvents, confirming the consolidation is safe. The merged array structure aligns with the payload-centric event flow and reduces consumer complexity.

src/enums.ts (1)

32-36: Event surface change verification complete. No stale references found.

Request* events are absent from active code; all references are confined to deprecated/. Payload* events are properly defined in src/enums.ts, tracked in src/events.ts, and actively emitted across contracts/evmx/watcher/Watcher.sol at appropriate points (PayloadStored, PayloadResolved, PayloadCancelled, PayloadSettled). The transition is clean and complete.

contracts/evmx/interfaces/IConfigurations.sol (1)

46-47: Interface change properly implemented. No active code issues.

The signature change from 4 to 3 parameters has been correctly handled:

  • Implementation (contracts/evmx/watcher/Configurations.sol:120) now accepts 3 params and infers appGateway_ from msg.sender
  • Active call site (contracts/evmx/base/AppGatewayBase.sol:154) uses the correct 3-arg signature
  • Deprecated code has incompatible 4-arg calls, but this is not part of the active contracts

No fixes needed.

contracts/evmx/watcher/precompiles/ReadPrecompile.sol (1)

45-65: Validation and encoding are solid

Checks target/payload, computes deadline, and emits with payloadId. No issues.

contracts/evmx/watcher/Watcher.sol (1)

256-259: Suggested fix is incompatible with function signature

The unblockAndAssignCredits method accepts only 2 parameters (bytes32 payloadId_, address assignTo_), not 3. Passing feesUsed_ as shown in the suggested diff would cause a compilation error.

If feesUsed_ tracking is necessary for accurate settlement, the function signature itself needs modification—both in FeesManager.sol and IFeesManager.sol—before the call site can be updated. This is a different change than what the review proposes.

Likely an incorrect or invalid review comment.

contracts/evmx/interfaces/IWatcher.sol (1)

8-20: Clean interface migration to payload-centric model.

The new interface properly defines payload lifecycle events and aligns with the architectural shift from queue/request-centric to payload-centric model.

contracts/evmx/watcher/Configurations.sol (4)

37-38: LGTM: State variables support signature validation.

These variables properly enable nonce-based replay protection and chain-specific digest computation.


187-195: LGTM: Standard EIP-191 signature recovery.

The implementation correctly applies the Ethereum Signed Message prefix and recovers the signer.


78-91: Both structs are properly defined and correctly used.

WatcherMultiCallParams and AppGatewayConfig are defined in contracts/utils/common/Structs.sol, and all field accesses in the function match their definitions. No issues found.


120-123: Verify callers updated for msg.sender context.

The function now uses msg.sender instead of an explicit appGateway_ parameter. This is more secure but is a breaking change. Ensure all calling code has been updated to call this function from the correct context (the app gateway itself must call it).

Comment on lines 270 to +276
) internal async {
_setMaxFees(getMaxFees(chainSlug_));
_setOverrides(consumeFrom_);
overrideParams = overrideParams.setMaxFees(getMaxFees(chainSlug_)).setConsumeFrom(
consumeFrom_
);

QueueParams memory queueParams;
queueParams.overrideParams = overrideParams;
queueParams.transaction = Transaction({
RawPayload memory rawPayload;
rawPayload.overrideParams = overrideParams;
rawPayload.transaction = Transaction({
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

Invalid Solidity syntax and undeclared identifiers in _createRequest

  • async is not valid Solidity syntax.
  • overrideParams is undeclared.
  • RawPayload and Transaction types aren’t imported.

This should declare a local OverrideParams, import types, and remove async.

-function _createRequest(
+function _createRequest(
     uint32 chainSlug_,
     address consumeFrom_,
     uint256 maxFees_,
     bytes memory payload_
-) internal async {
-        overrideParams = overrideParams.setMaxFees(getMaxFees(chainSlug_)).setConsumeFrom(
-            consumeFrom_
-        );
-
-        RawPayload memory rawPayload;
-        rawPayload.overrideParams = overrideParams;
+) internal {
+        OverrideParams memory params;
+        params = OverrideParamsLib
+            .setMaxFees(params, getMaxFees(chainSlug_))
+            .setConsumeFrom(consumeFrom_);
+
+        RawPayload memory rawPayload;
+        rawPayload.overrideParams = params;
         rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
         watcher__().addPayloadData(rawPayload, address(this));
 }

Also add missing imports (see next comment).

Also applies to: 274-281

🤖 Prompt for AI Agents
In contracts/evmx/fees/Credit.sol around lines 269-276 (and similarly 274-281)
the function contains invalid Solidity syntax and undeclared identifiers: remove
the JavaScript-style "async", declare a local OverrideParams variable (e.g.,
OverrideParams memory overrideParams = OverrideParams(...); or OverrideParams
memory overrideParams; then set fields) before calling setMaxFees and
setConsumeFrom, and ensure the RawPayload and Transaction types (and any
OverrideParams type) are imported or defined in this file via the correct
Solidity import statements; update the code to assign overrideParams on a local
memory variable, populate rawPayload.transaction with a properly constructed
Transaction struct, and remove any JS-specific keywords so the code is valid
Solidity.

Comment on lines 92 to 97
/// @notice Blocks fees for a request count
/// @param requestCount_ The batch identifier
/// @param payloadId_ The payload id
/// @param consumeFrom_ The fees payer address
/// @param credits_ The total fees to block
/// @dev Only callable by delivery helper
function blockCredits(
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

Docstring drift.

“This should” say “Blocks fees for a payload” not “request count”. Update the NatSpec to match the payloadId flow.

-    /// @notice Blocks fees for a request count
+    /// @notice Blocks fees for a payload
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Blocks fees for a request count
/// @param requestCount_ The batch identifier
/// @param payloadId_ The payload id
/// @param consumeFrom_ The fees payer address
/// @param credits_ The total fees to block
/// @dev Only callable by delivery helper
function blockCredits(
/// @notice Blocks fees for a payload
/// @param payloadId_ The payload id
/// @param consumeFrom_ The fees payer address
/// @param credits_ The total fees to block
/// @dev Only callable by delivery helper
function blockCredits(
🤖 Prompt for AI Agents
In contracts/evmx/fees/FeesManager.sol around lines 92 to 97, the NatSpec
title/description is incorrect: it currently says "Blocks fees for a request
count" but the function operates on a payloadId; update the docstring to say
"Blocks fees for a payload" (and adjust any other mentions of "request count" to
"payload" or "payloadId" so the documentation matches the function parameters
and behavior).

Comment on lines +48 to 49
function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external;

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

Use calldata for external struct param to avoid unnecessary copies.

This should be calldata for gas and ABI consistency; interfaces constrain implementers.

-    function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external;
+    function setAppGatewayConfigs(WatcherMultiCallParams calldata params_) external;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external;
function setAppGatewayConfigs(WatcherMultiCallParams calldata params_) external;
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IConfigurations.sol around lines 48-49, the
external function parameter of type WatcherMultiCallParams should be declared as
calldata to avoid unnecessary memory copies and enforce ABI consistency; update
the interface signature to use `WatcherMultiCallParams calldata params_`, and
ensure all implementing contracts update their overriding function signatures to
accept the struct as calldata as well.

/// @notice Configuration contract for the Watcher Precompile system
/// @dev Handles the mapping between networks, plugs, and app gateways for payload execution
contract Configurations is ConfigurationsStorage, Initializable, Ownable, WatcherBase {
contract Configurations is ConfigurationsStorage, Ownable, AddressResolverUtil {
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

🧩 Analysis chain

Verify initialization mechanism.

The contract removed Initializable from inheritance but added evmxSlug state variable (line 37). Ensure there's a mechanism to initialize evmxSlug when the contract is deployed, as it's used in signature validation (line 176).


🏁 Script executed:

#!/bin/bash
# Search for initialization of evmxSlug
rg -n --type=sol 'evmxSlug\s*=' -A2 -B2

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for evmxSlug initialization without type restriction
rg -n 'evmxSlug' -A3 -B3 contracts/evmx/watcher/Configurations.sol

Length of output: 518


🏁 Script executed:

#!/bin/bash
# Look for constructor in Configurations contract
rg -n 'constructor\s*\(' contracts/evmx/watcher/Configurations.sol

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for initialize function
rg -n 'function\s+initialize' contracts/evmx/watcher/Configurations.sol

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full context of the contract to understand initialization strategy
head -100 contracts/evmx/watcher/Configurations.sol

Length of output: 4223


🏁 Script executed:

#!/bin/bash
# Search for any setter or assignment to evmxSlug
rg -n 'evmxSlug\s*=' contracts/evmx/watcher/Configurations.sol

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for all function definitions in the contract
rg -n 'function\s+\w+' contracts/evmx/watcher/Configurations.sol | head -20

Length of output: 563


🏁 Script executed:

#!/bin/bash
# Get the rest of the file to see all functions
tail -n +100 contracts/evmx/watcher/Configurations.sol | head -150

Length of output: 4073


This should provide a setter function for evmxSlug or initialize it in a constructor.

The contract removed Initializable from inheritance without providing an alternative way to initialize evmxSlug. Since this variable is used in signature validation at line 176 and defaults to 0, signatures will always be computed with the wrong chain slug value. Either add a constructor that accepts evmxSlug as a parameter, or add a setter function (likely onlyOwner).

🤖 Prompt for AI Agents
In contracts/evmx/watcher/Configurations.sol around line 49, the contract
removed Initializable but never sets evmxSlug, so it remains zero and breaks
signature validation at line 176; add an initialization path: either implement a
constructor that accepts a uint256 evmxSlug parameter and sets the storage
variable, or add an onlyOwner setter function setEvmxSlug(uint256) that updates
the variable and emits an event; ensure the chosen method sets evmxSlug before
any signature validation can occur and include access control (onlyOwner) and an
event for state change.

Comment on lines +246 to 254
function cancelExecution(bytes32 payloadId_) public {
Payload storage r = _payloads[payloadId_];
if (r.isPayloadExecuted) revert PayloadAlreadySettled();
if (r.isPayloadCancelled) revert PayloadAlreadyCancelled();

function cancelRequest(uint40 requestCount_) external override {
requestHandler__.cancelRequest(requestCount_, msg.sender);
r.isPayloadCancelled = true;
_settlePayload(payloadId_, r.maxFees);
emit PayloadCancelled(payloadId_);
}
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

Cancellation charges maxFees

cancelExecution settles with r.maxFees, which charges full reserved amount even if nothing executed. This should refund or settle with actual used fees (often zero).

Apply:

-    _settlePayload(payloadId_, r.maxFees);
+    _settlePayload(payloadId_, 0);

If you keep reserving maxFees upfront, ensure you track and pass the true feesUsed_ on resolution paths.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function cancelExecution(bytes32 payloadId_) public {
Payload storage r = _payloads[payloadId_];
if (r.isPayloadExecuted) revert PayloadAlreadySettled();
if (r.isPayloadCancelled) revert PayloadAlreadyCancelled();
function cancelRequest(uint40 requestCount_) external override {
requestHandler__.cancelRequest(requestCount_, msg.sender);
r.isPayloadCancelled = true;
_settlePayload(payloadId_, r.maxFees);
emit PayloadCancelled(payloadId_);
}
function cancelExecution(bytes32 payloadId_) public {
Payload storage r = _payloads[payloadId_];
if (r.isPayloadExecuted) revert PayloadAlreadySettled();
if (r.isPayloadCancelled) revert PayloadAlreadyCancelled();
r.isPayloadCancelled = true;
_settlePayload(payloadId_, 0);
emit PayloadCancelled(payloadId_);
}
🤖 Prompt for AI Agents
In contracts/evmx/watcher/Watcher.sol around lines 246 to 254, cancelExecution
currently settles with r.maxFees which charges the full reserved amount even
when nothing was used; change the logic to settle using the actual fees used
(feesUsed) instead: ensure Payload struct tracks feesUsed (or compute actual
used fees before cancelling), pass that feesUsed value to _settlePayload instead
of r.maxFees, and if fees were reserved up-front refund the unused portion by
subtracting feesUsed from reserved amount during settlement; keep setting
r.isPayloadCancelled = true and emitting PayloadCancelled after updating
feesUsed and calling _settlePayload.

export const REQUIRED_ROLES = {
EVMx: {
AuctionManager: [ROLES.TRANSMITTER_ROLE],
// AuctionManager: [ROLES.TRANSMITTER_ROLE],
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

🧩 Analysis chain

Roles pruned in REQUIRED_ROLES; align EVMx role setup and watcher targeting

This should keep role assignment consistent. You still grant TRANSMITTER_ROLE to AuctionManager in setRolesForEVMx while AuctionManager is commented in REQUIRED_ROLES. Confirm AM is still deployed/used in Phase 1; otherwise remove that grant.

Replace contract allowlist in watcher targeting with mapping‑driven check to avoid stale includes:

-        [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard].includes(
-          contractName as Contracts
-        ) && roleName === ROLES.WATCHER_ROLE
+        roleName === ROLES.WATCHER_ROLE &&
+        (contractName as Contracts) === Contracts.FastSwitchboard

Also applies to: 33-35, 41-41


🏁 Script executed:

#!/bin/bash
# Verify usage and address presence
rg -nP 'Contracts\.AuctionManager|setDefaultAuctionManager' -C2
rg -nP '"AuctionManager"\s*:' addresses 2>/dev/null || true

Length of output: 1776


🏁 Script executed:

#!/bin/bash
# Check REQUIRED_ROLES object and surrounding context
rg -nP 'REQUIRED_ROLES\s*=|AuctionManager' hardhat-scripts/deploy/2.roles.ts -A30 | head -80

Length of output: 2137


Remove AuctionManager role grant to match REQUIRED_ROLES cleanup

AuctionManager was pruned from REQUIRED_ROLES.EVMx (line 28, commented out), but the setRoleForContract call granting TRANSMITTER_ROLE to AuctionManager at lines 123–129 remains active. Remove or comment out lines 123–129 to keep role setup consistent with the REQUIRED_ROLES change.

The optional refactor for watcher targeting logic (lines 32–35, 41) still applies—replace the contract allowlist check with direct comparison to avoid stale includes:

-        [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard].includes(
-          contractName as Contracts
-        ) && roleName === ROLES.WATCHER_ROLE
+        roleName === ROLES.WATCHER_ROLE &&
+        (contractName as Contracts) === Contracts.FastSwitchboard

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In hardhat-scripts/deploy/2.roles.ts (around lines 28, 32–35, 41 and 123–129):
the AuctionManager was removed from REQUIRED_ROLES but the code that grants
TRANSMITTER_ROLE to AuctionManager at lines 123–129 is still active; remove or
comment out those lines (123–129) so role setup matches REQUIRED_ROLES, and
optionally update the watcher targeting logic at lines 32–35 and 41 by replacing
the allowlist/includes check with a direct contract equality comparison to avoid
stale includes.

Comment on lines 58 to 68
// await updateContractSettings(
// EVMX_CHAIN_ID,
// Contracts.AddressResolver,
// "defaultAuctionManager",
// [],
// evmxAddresses[Contracts.AuctionManager],
// "setDefaultAuctionManager",
// [evmxAddresses[Contracts.AuctionManager]],
// signer
// );

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

🧩 Analysis chain

Critical config steps disabled; guard with flags and confirm replacement paths

Commenting out defaultAuctionManager, deployForwarder, and setWatcherCoreContracts changes system wiring. This should be gated and documented, or equivalent config must happen elsewhere to avoid runtime gaps.

-// await updateContractSettings( ... setDefaultAuctionManager ... );
+if (process.env.SET_DEFAULT_AUCTION_MANAGER === "1") {
+  await updateContractSettings(/* setDefaultAuctionManager params */);
+}
-// await updateContractSettings( ... setDeployForwarder ... );
+if (process.env.SET_DEPLOY_FORWARDER === "1") {
+  await updateContractSettings(/* setDeployForwarder params */);
+}
-// await setWatcherCoreContracts(evmxAddresses);
+if (process.env.SET_WATCHER_CORE_CONTRACTS === "1") {
+  await setWatcherCoreContracts(evmxAddresses);
+}

Explain where these are now configured (file/step), or run targeted checks post‑deploy to ensure state is set.

Also applies to: 80-90, 124-124


🏁 Script executed:

#!/bin/bash
# View the actual file content around the flagged line ranges
echo "=== Lines 50-75 (context around 58-68) ==="
sed -n '50,75p' hardhat-scripts/deploy/4.configureEVMx.ts

echo -e "\n=== Lines 75-100 (context around 80-90) ==="
sed -n '75,100p' hardhat-scripts/deploy/4.configureEVMx.ts

echo -e "\n=== Lines 115-130 (context around 124) ==="
sed -n '115,130p' hardhat-scripts/deploy/4.configureEVMx.ts

Length of output: 1766


🏁 Script executed:

#!/bin/bash
# Search for updateContractSettings calls (active ones)
echo "=== Active updateContractSettings calls ==="
rg -n 'await updateContractSettings' hardhat-scripts/deploy/ -A2 -B1

echo -e "\n=== Active setWatcherCoreContracts calls ==="
rg -n 'await setWatcherCoreContracts' hardhat-scripts/deploy/ -A2 -B1

Length of output: 4206


🏁 Script executed:

#!/bin/bash
# Search for AuctionManager, DeployForwarder config elsewhere
echo "=== setDefaultAuctionManager references ==="
rg -n 'setDefaultAuctionManager|defaultAuctionManager' --type ts -C2

echo -e "\n=== setDeployForwarder references ==="
rg -n 'setDeployForwarder|DeployForwarder' --type ts -C2

echo -e "\n=== setWatcherCoreContracts references ==="
rg -n 'setWatcherCoreContracts' --type ts -C2

Length of output: 2097


Three critical configurations disabled; no equivalent paths found elsewhere—must be restored or explicitly justified

Verification confirms all three disabled steps are missing:

  • setDefaultAuctionManager: no active calls exist anywhere
  • setDeployForwarder: no active calls exist anywhere
  • setWatcherCoreContracts: function defined (line 168) but never invoked

The interspersed active calls (setFeesManager, setWatcher, setPrecompile) show configuration intentionally happens here. These three commented blocks represent incomplete state setup that will cause runtime gaps.

This should either be restored and executed unconditionally, or if intentionally disabled, that decision must be documented with clear reasoning in a code comment explaining why these steps are skipped.

🤖 Prompt for AI Agents
In hardhat-scripts/deploy/4.configureEVMx.ts around lines 58 to 68 (and note the
setWatcherCoreContracts definition at ~line 168 is never called), the three
commented configuration steps (setDefaultAuctionManager, setDeployForwarder, and
invoking setWatcherCoreContracts) are disabled and missing elsewhere; either
restore and execute these calls unconditionally in the appropriate sequence here
(re-enable the commented updateContractSettings calls for
setDefaultAuctionManager and setDeployForwarder and add a call to
setWatcherCoreContracts after related setup), or if they must remain disabled,
add a clear code comment at the top of this block explaining why each step is
intentionally skipped, referencing the missing alternative config paths and any
risk/mitigation, so reviewers/runtime users understand the rationale.

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: 8

Caution

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

⚠️ Outside diff range comments (2)
script/counter/SetFees.s.sol (1)

21-25: Remove commented code or implement the script properly.

This script is named CounterSetFees but doesn't set fees—the logic is commented out. This creates confusion and maintenance burden.

Either remove the commented lines if they're obsolete, or implement the fee-setting functionality if the script is needed. If this is a template for manual customization, add a clear comment explaining that.

hardhat-scripts/deploy/2.roles.ts (1)

117-128: Fix signer selection for EVMx role grant

getSigner(EVMX_CHAIN_ID, true) hands you the watcher wallet, but FeesPool only accepts grantRole from its admin (the same socket signer you use everywhere else). This should stay on the admin signer; otherwise the call reverts and Phase 1 setup never finishes. Replace this with the socket signer (omit the true flag or call getSocketSigner directly) so the admin actually executes the grant.

-  const signer = await getSigner(EVMX_CHAIN_ID, true);
+  const signer = await getSigner(EVMX_CHAIN_ID);
♻️ Duplicate comments (7)
hardhat-scripts/deploy/1.deploy.ts (1)

176-188: The past review concern about missing triggerFees appears addressed.

The deployment now passes 5 parameters: EVMX_CHAIN_ID, EVMxOwner, addressResolver.address, transmitter, and TRIGGER_FEES. Verify this matches the Watcher.initialize signature.

#!/bin/bash
# Verify Watcher.initialize signature matches the deployment parameters
ast-grep --pattern $'function initialize($$$) {
  $$$
}' contracts/evmx/watcher/Watcher.sol
contracts/utils/common/Structs.sol (1)

143-147: Verify consumers access switchboardType via overrideParams.

The past review comment mentioned that RawPayload is missing switchboardType. The field is now nested in overrideParams.switchboardType. Ensure all consumers (e.g., WritePrecompile, ForwarderV2) access it via the nested path rawPayload.overrideParams.switchboardType.

#!/bin/bash
# Search for rawPayload.switchboardType usage to verify correct access pattern
rg -nP 'rawPayload\.switchboardType' --type=sol
contracts/evmx/watcher/Watcher.sol (3)

86-95: Block actual fees after computing them, not maxFees upfront.

Lines 86-90 block maxFees before the precompile computes the actual fees at lines 93-94. This over-reserves credits. The flow should: compute fees first, validate against maxFees, then block only the computed fee amount.

Apply this diff:

         IPrecompile precompile = IPrecompile(precompiles[payloadData.overrideParams.callType]);
         if (address(precompile) == address(0)) revert InvalidCallType();
 
-        feesManager__().blockCredits(
-            currentPayloadId,
-            payloadData.overrideParams.consumeFrom,
-            payloadData.overrideParams.maxFees
-        );
         asyncPromise = latestAsyncPromise;
 
         (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile(precompile)
             .handlePayload(payloadData, latestAppGateway, currentPayloadId);
         if (fees > payloadData.overrideParams.maxFees) revert InsufficientFees();
+        
+        feesManager__().blockCredits(
+            currentPayloadId,
+            payloadData.overrideParams.consumeFrom,
+            fees
+        );

258-261: Pass feesUsed_ to unblockAndAssignCredits.

The feesUsed_ parameter is accepted but not passed to unblockAndAssignCredits. The FeesManager needs this value to properly refund unused credits vs. charge actual fees used.

Verify the unblockAndAssignCredits signature and apply:

 function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal {
-    feesManager__().unblockAndAssignCredits(payloadId_, transmitter);
+    feesManager__().unblockAndAssignCredits(payloadId_, transmitter, feesUsed_);
     emit PayloadSettled(payloadId_);
 }

248-256: Cancellation should not charge maxFees.

cancelExecution settles with r.maxFees at line 254, charging the full reserved amount even when nothing executed. This should settle with actual fees used (likely 0 for cancelled payloads).

     r.isPayloadCancelled = true;
-    _settlePayload(payloadId_, r.maxFees);
+    _settlePayload(payloadId_, 0);
     emit PayloadCancelled(payloadId_);
contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)

10-10: Add missing RawPayload import.

RawPayload is used in the event (line 60) and function signatures (line 101) but not imported from Structs.sol. This will cause compilation failure.

-import {DigestParams, Payload} from "../../../utils/common/Structs.sol";
+import {DigestParams, Payload, RawPayload} from "../../../utils/common/Structs.sol";

174-187: gasLimit returns 0 when override is provided.

The function only assigns gasLimit inside the if block when overrideParams.gasLimit == 0. When a caller provides a non-zero override, gasLimit remains at its default value of 0, which flows into the digest hash causing incorrect behavior.

Initialize gasLimit from the override first, then apply chain defaults only when override is 0:

     function _validate(
         RawPayload calldata rawPayload_,
         address appGateway_
     ) internal view returns (uint256 gasLimit) {
+        gasLimit = rawPayload_.overrideParams.gasLimit;
+        
         if (
             rawPayload_.overrideParams.value >
             chainMaxMsgValueLimit[rawPayload_.transaction.chainSlug]
         ) revert MaxMsgValueLimitExceeded();
 
         if (
             rawPayload_.transaction.payload.length == 0 ||
             rawPayload_.transaction.payload.length > PAYLOAD_SIZE_LIMIT
         ) {
             revert InvalidPayloadSize();
         }
 
         if (rawPayload_.transaction.target == bytes32(0)) revert InvalidTarget();
 
         watcher__.verifyConnections(
             rawPayload_.transaction.chainSlug,
             rawPayload_.transaction.target,
             appGateway_,
             rawPayload_.overrideParams.switchboardType
         );
 
         // todo: can be changed to set the default gas limit for each chain
-        if (rawPayload_.overrideParams.gasLimit == 0) {
+        if (gasLimit == 0) {
             if (rawPayload_.transaction.chainSlug == 5000) {
                 // Mantle default gas limit
                 gasLimit = 8_000_000_000;
             } else if (rawPayload_.transaction.chainSlug == 1329) {
                 // Sei default gas limit
                 gasLimit = 8_000_000;
             } else if (rawPayload_.transaction.chainSlug == 999) {
                 // HyperEVM default gas limit
                 gasLimit = 1_500_000;
             } else {
                 gasLimit = 10_000_000; // other chains default gas limit
             }
         }
     }
🧹 Nitpick comments (8)
contracts/protocol/switchboard/FastSwitchboard.sol (1)

14-16: Update mapping documentation to use digest terminology.

The comment refers to payloadId but the mapping is now exclusively accessed using digest variables throughout the contract. Update the comment for consistency.

Apply this diff to align the documentation:

-    // used to track if watcher have attested a payload
-    // payloadId => isAttested
+    // used to track if watcher have attested a payload
+    // digest => isAttested
     mapping(bytes32 => bool) public isAttested;
deprecated/script/helpers/DepositCredit.s.sol (1)

12-12: Make fee amount configurable.

The feesAmount is hardcoded to 10 USDC. This should be configurable via environment variable for flexibility, consistent with other script parameters.

Apply this diff:

-        uint256 feesAmount = 10000000; // 10 USDC
+        uint256 feesAmount = vm.envUint("FEES_AMOUNT"); // in wei (e.g., 10000000 for 10 USDC)
src/types.ts (1)

35-36: Document the purpose of new fields.

The new fields lack documentation. Add JSDoc comments explaining what ERC1967Factory, FeesPool, and transmitterEOA represent, especially since this is a payload-first migration that changes the architectural pattern.

Example documentation:

 export type EVMxAddressesObj = {
+  /** Address of the ERC1967 proxy factory contract */
   ERC1967Factory: string;
+  /** Address of the fees pool contract */
   FeesPool: string;
   AddressResolver: string;
 export type S3Config = {
   version: string;
+  /** EOA address of the transmitter account */
   transmitterEOA: string;
   chains: { [chainSlug: number]: ChainConfig };

Also applies to: 49-49

script/counter/IncrementCountersFromApp.s.sol (2)

20-20: Remove unused counter variable.

The counter variable is declared but never used. Either use it or remove it to clean up the code.

-        address counter = vm.envAddress("PLUG");
-

22-22: Clarify the commented uploadPlug call.

The uploadPlug call is commented out. If this functionality is still needed, uncomment and implement it. If it's deprecated, remove the commented code entirely.

hardhat-scripts/deploy/1.deploy.ts (1)

311-319: Remove commented ContractFactoryPlug deployment block.

If ContractFactoryPlug is deprecated, remove this commented code entirely. If it's temporarily disabled, add a comment explaining why and when it will be re-enabled.

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

124-135: Document the purpose of new OverrideParams fields.

Several new fields were added: consumeFrom, switchboardType, maxFees. Add inline comments explaining their purpose and constraints to improve maintainability.

contracts/evmx/helpers/AddressResolverUtil.sol (1)

29-34: Fix misleading function documentation.

The comment on line 29 says "Restricts function access to the watcher owner" but the function returns true for both watcher__() address AND the watcher's owner. This should say "Checks if caller is watcher or watcher owner" or similar.

-    /// @notice Restricts function access to the watcher owner
+    /// @notice Checks if the caller is the watcher contract or watcher owner
     function isWatcher() internal view returns (bool) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9aae799 and ba5a850.

📒 Files selected for processing (36)
  • .env.sample (1 hunks)
  • Errors.md (2 hunks)
  • EventTopics.md (5 hunks)
  • FunctionSignatures.md (8 hunks)
  • contracts/evmx/helpers/AddressResolverUtil.sol (1 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/watcher/Configurations.sol (6 hunks)
  • contracts/evmx/watcher/Watcher.sol (3 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (4 hunks)
  • contracts/protocol/switchboard/FastSwitchboard.sol (1 hunks)
  • contracts/utils/common/Structs.sol (3 hunks)
  • deprecated/script/helpers/DepositCredit.s.sol (1 hunks)
  • foundry.toml (1 hunks)
  • hardhat-scripts/config/config.ts (3 hunks)
  • hardhat-scripts/constants/feeConstants.ts (1 hunks)
  • hardhat-scripts/constants/types.ts (1 hunks)
  • hardhat-scripts/deploy/1.deploy.ts (3 hunks)
  • hardhat-scripts/deploy/2.roles.ts (1 hunks)
  • hardhat-scripts/deploy/3.configureChains.ts (3 hunks)
  • hardhat-scripts/deploy/4.configureEVMx.ts (4 hunks)
  • hardhat-scripts/deploy/6.connect.ts (4 hunks)
  • hardhat-scripts/deploy/7.upload.ts (1 hunks)
  • hardhat-scripts/s3Config/buildConfig.ts (2 hunks)
  • hardhat-scripts/utils/sign.ts (2 hunks)
  • hardhat.config.ts (1 hunks)
  • package.json (1 hunks)
  • script/counter/DeployCounterPlug.s.sol (1 hunks)
  • script/counter/DeployEVMxCounterApp.s.sol (1 hunks)
  • script/counter/IncrementCountersFromApp.s.sol (1 hunks)
  • script/counter/ReadOnchainCounters.s.sol (1 hunks)
  • script/counter/SetFees.s.sol (1 hunks)
  • script/counter/WithdrawFeesArbitrumFeesPlug.s.sol (1 hunks)
  • src/enums.ts (2 hunks)
  • src/events.ts (2 hunks)
  • src/types.ts (1 hunks)
  • test/SetupTest.t.sol (17 hunks)
✅ Files skipped from review due to trivial changes (1)
  • script/counter/WithdrawFeesArbitrumFeesPlug.s.sol
🚧 Files skipped from review as they are similar to previous changes (3)
  • hardhat-scripts/deploy/4.configureEVMx.ts
  • package.json
  • src/enums.ts
🧰 Additional context used
🧬 Code graph analysis (3)
hardhat-scripts/s3Config/buildConfig.ts (1)
hardhat-scripts/config/config.ts (1)
  • transmitter (179-179)
hardhat-scripts/deploy/6.connect.ts (5)
hardhat-scripts/utils/deployUtils.ts (1)
  • getInstance (131-135)
hardhat-scripts/utils/sign.ts (1)
  • signWatcherMessage (29-41)
hardhat-scripts/constants/types.ts (1)
  • WatcherMultiCallParams (7-12)
hardhat-scripts/utils/overrides.ts (1)
  • overrides (94-105)
hardhat-scripts/config/config.ts (1)
  • EVMX_CHAIN_ID (177-177)
hardhat-scripts/deploy/1.deploy.ts (1)
hardhat-scripts/config/config.ts (2)
  • transmitter (179-179)
  • TRIGGER_FEES (285-285)
🪛 dotenv-linter (4.0.0)
.env.sample

[warning] 24-24: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🔇 Additional comments (25)
script/counter/ReadOnchainCounters.s.sol (1)

6-7: Clean import path refactor.

The import paths correctly reflect the new directory structure. No functional changes to the script logic.

contracts/protocol/switchboard/FastSwitchboard.sol (1)

23-23: Event parameter rename aligns with payload-first architecture.

The parameter name change from payloadId_ to digest correctly reflects the architectural shift described in the PR. This aligns with the emit statement on line 53 and the digest_ parameter naming used throughout the contract's functions.

script/counter/SetFees.s.sol (1)

6-6: Import path change verified and valid.

The new path ../../test/apps/counter/CounterAppGateway.sol resolves correctly to an existing file. The reorganization of test assets is consistent across the change.

script/counter/DeployEVMxCounterApp.s.sol (1)

6-6: LGTM - Import path updated correctly.

The import path reflects the reorganization of counter app files from test/apps/app-gateways/counter/ to test/apps/counter/.

script/counter/DeployCounterPlug.s.sol (1)

11-18: LGTM - Standard deployment setup.

Environment variable configuration and fork setup follow standard Forge deployment patterns.

deprecated/script/helpers/DepositCredit.s.sol (1)

1-35: Verify placement in deprecated directory.

This is a new script added directly to the deprecated/ directory. Clarify whether this script is:

  • Temporary for Phase 1 migration testing only
  • Supporting legacy infrastructure that will be removed
  • Misplaced and should be in an active scripts directory

If this script has ongoing utility, it should not be in deprecated/.

src/types.ts (2)

35-36: This review comment is incorrect. The original concern mischaracterizes how EVMxAddressesObj is used. The type isn't constructed directly in TypeScript code—it's loaded from deployment JSON files and cast. Since all deployment JSON files (dev_addresses.json, prod_addresses.json, stage_addresses.json) already contain both ERC1967Factory and FeesPool, the type definition change reflects existing data with no breaking changes in actual code.

Likely an incorrect or invalid review comment.


49-49: No action required. Breaking change properly handled.

Verification confirms the transmitterEOA field addition has been correctly implemented. The codebase contains only one S3Config construction site (buildConfig.ts:34-39), and it's already updated with the new required field. The transmitter value is sourced from getCurrentModeConfig(), which validates the mode exists before returning, guaranteeing the field will always be populated. No orphaned construction code exists elsewhere.

test/SetupTest.t.sol (10)

63-63: LGTM: Fee amount constant added.

The feesAmount constant is properly used in _resolvePayload for deposit handling.


108-108: Verify the test funding amount.

Funding the fees pool with 100000 ether is extremely high for tests. Confirm this amount is necessary for test scenarios or reduce to a more reasonable value like 1000 ether.


125-127: LGTM: Precompile setup is clean.

Direct precompile registration via watcher.setPrecompile is straightforward and properly authorized within the watcher EOA prank block.


147-185: LGTM: Gateway setup refactored correctly.

The setupGatewayAndPlugs consolidates gateway and plug wiring into a single path. Authorization is properly handled via hoax(watcherEOA) and signature verification.


439-452: LGTM: Deposit flow simplified.

Direct interaction with feesManager is cleaner than the previous approach. Balance assertions correctly verify credit and native deposits.


610-612: LGTM: Authorization handled correctly.

Using hoax(watcherEOA) before writePrecompile.uploadProof properly grants watcher authority for the call.


758-820: LGTM: Payload validation complete.

The checkPayload function is fully migrated to use Payload[] with comprehensive field assertions. All new fields (consumeFrom, maxFees) are properly validated.


721-744: No issues found. Authorization is properly implemented.

Both watcher.resolvePayload and watcher.markRevert validate signatures via _validateSignature before executing any logic. The signature-based authorization using WatcherMultiCallParams is the intended and correct mechanism—not a deviation from standard patterns. The nonce prevents replay attacks. Tests using _createWatcherSignature are following the proper authorization flow.


524-552: This review comment is incorrect.

The concern about underflow is unfounded. Across all precompiles (ReadPrecompile, WritePrecompile, SchedulePrecompile), deadline is consistently set by adding expiryTime to a current/future timestamp: deadline = block.timestamp + expiryTime (or executeAfter + expiryTime). This guarantees deadline >= block.timestamp + expiryTime, making deadline - expiryTime >= block.timestamp always true. The test's time warping to deadline - expiryTime and deadline is safe—no underflow occurs.

Likely an incorrect or invalid review comment.


653-653: bytes32(0) for prevBatchDigestHash is correct and intentional.

The digest construction logic includes prevBatchDigestHash directly in the keccak256 encoding without validation or special handling. Using bytes32(0) for initial/standalone payloads is the intended pattern, consistent across both test files and deprecated implementations. No changes needed.

hardhat.config.ts (1)

34-36: No action required—.env.sample is already updated.

The verification confirms that .env.sample line 24 already contains SOCKET_PRIVATE_KEY with the correct variable name, matching the code change in hardhat.config.ts. Documentation and environment configuration are in sync.

script/counter/IncrementCountersFromApp.s.sol (1)

33-33: No issues found. The signature matches correctly.

The current CounterAppGateway.incrementCounters in test/apps/counter/CounterAppGateway.sol:29 expects a single address parameter, and the call passes exactly that. The change from array-based to single-address is properly implemented.

EventTopics.md (1)

118-134: All documented IWatcher events are declared in the interface with matching signatures.

Verification confirms all 12 events documented in EventTopics.md (lines 118-134) are present in IWatcher.sol. Event names, argument types, and parameter counts match the interface declarations.

src/events.ts (1)

12-24: All removed event exports have no active consumers—change is safe.

Verification confirms promiseResolverEvents, requestHandlerEvents, configurationsEvents, and auctionManagerEvents are not imported anywhere in the codebase. No other files require updates.

hardhat-scripts/constants/feeConstants.ts (1)

345-345: Review comment is resolved - empty string handling already in place.

The codebase contains only one consumer of getFeePool() at hardhat-scripts/deploy/1.deploy.ts:114, and it already validates the empty string case with if (feePool?.length == 0) on line 115. No other callsites exist, so no additional changes are needed.

foundry.toml (2)

12-28: Configuration updated correctly for Phase 1 migration.

All label entries have been remapped to new addresses as expected for the architectural refactor. The mapping includes all core components: address resolution, async deployment, precompiles, watcher, and fee management. No duplicate addresses or formatting issues detected.


12-28: Verify deployed contract addresses match the updated [labels] mapping.

Basic format validation passes: all 16 addresses are unique and follow proper formatting (0x + 40 hex characters). However, EIP-55 checksum correctness and chain verification still require manual confirmation. Verify these addresses resolve to actual deployed contracts on your target network by running:

cast code <address> --rpc-url <YOUR_RPC>

For each address in the mapping.

import {FeesPlug} from "../../contracts/evmx/plugs/FeesPlug.sol";
import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol";

// source .env && forge script script/helpers/DepositCreditAndNative.s.sol --broadcast --skip-simulation
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

Fix incorrect filename in comment.

The comment references DepositCreditAndNative.s.sol but the actual file is DepositCredit.s.sol.

Apply this diff:

-// source .env && forge script script/helpers/DepositCreditAndNative.s.sol --broadcast --skip-simulation
+// source .env && forge script script/helpers/DepositCredit.s.sol --broadcast --skip-simulation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// source .env && forge script script/helpers/DepositCreditAndNative.s.sol --broadcast --skip-simulation
// source .env && forge script script/helpers/DepositCredit.s.sol --broadcast --skip-simulation
🤖 Prompt for AI Agents
In deprecated/script/helpers/DepositCredit.s.sol around line 9, the shell
command comment references the wrong filename `DepositCreditAndNative.s.sol`;
update the comment to reference the actual file `DepositCredit.s.sol` (e.g.,
change the command to "source .env && forge script
script/helpers/DepositCredit.s.sol --broadcast --skip-simulation") so the
example matches the real script name.

Comment on lines +11 to +34
function run() external {
uint256 feesAmount = 10000000; // 10 USDC
vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC"));

uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));

// mint test USDC to sender
testUSDCContract.mint(vm.addr(privateKey), feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);

address sender = vm.addr(privateKey);
console.log("Sender address:", sender);
uint256 balance = testUSDCContract.balanceOf(sender);
console.log("Sender balance in wei:", balance);
console.log("App Gateway:", appGateway);
console.log("Fees Plug:", address(feesPlug));
console.log("Fees Amount:", feesAmount);
feesPlug.depositCredit(address(testUSDCContract), appGateway, feesAmount);
}
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

Missing vm.stopBroadcast() call.

The script calls vm.startBroadcast(privateKey) on line 16 but never calls vm.stopBroadcast(). This leaves the broadcast state open and can cause unintended transaction broadcasting in subsequent operations.

Add this after the depositCredit call:

         feesPlug.depositCredit(address(testUSDCContract), appGateway, feesAmount);
+        vm.stopBroadcast();
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function run() external {
uint256 feesAmount = 10000000; // 10 USDC
vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC"));
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));
// mint test USDC to sender
testUSDCContract.mint(vm.addr(privateKey), feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);
address sender = vm.addr(privateKey);
console.log("Sender address:", sender);
uint256 balance = testUSDCContract.balanceOf(sender);
console.log("Sender balance in wei:", balance);
console.log("App Gateway:", appGateway);
console.log("Fees Plug:", address(feesPlug));
console.log("Fees Amount:", feesAmount);
feesPlug.depositCredit(address(testUSDCContract), appGateway, feesAmount);
}
function run() external {
uint256 feesAmount = 10000000; // 10 USDC
vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC"));
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));
// mint test USDC to sender
testUSDCContract.mint(vm.addr(privateKey), feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);
address sender = vm.addr(privateKey);
console.log("Sender address:", sender);
uint256 balance = testUSDCContract.balanceOf(sender);
console.log("Sender balance in wei:", balance);
console.log("App Gateway:", appGateway);
console.log("Fees Plug:", address(feesPlug));
console.log("Fees Amount:", feesAmount);
feesPlug.depositCredit(address(testUSDCContract), appGateway, feesAmount);
vm.stopBroadcast();
}
🤖 Prompt for AI Agents
In deprecated/script/helpers/DepositCredit.s.sol around lines 11 to 34, the
script calls vm.startBroadcast(privateKey) but never calls vm.stopBroadcast(),
leaving the VM in broadcast mode; add a vm.stopBroadcast() call immediately
after the feesPlug.depositCredit(...) invocation (or otherwise ensure start/stop
are paired, e.g., wrap broadcasted operations and call vm.stopBroadcast() in all
execution paths) so broadcasting is properly terminated.

Comment on lines +15 to +26
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));

// mint test USDC to sender
testUSDCContract.mint(vm.addr(privateKey), feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);

address sender = vm.addr(privateKey);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Derive sender address once before broadcast.

The sender address is derived twice from privateKey (lines 22 and 26). This is redundant and inefficient.

Apply this diff to derive the address once:

         uint256 privateKey = vm.envUint("PRIVATE_KEY");
+        address sender = vm.addr(privateKey);
+        
         vm.startBroadcast(privateKey);
         FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
         address appGateway = vm.envAddress("APP_GATEWAY");
         TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));
 
         // mint test USDC to sender
-        testUSDCContract.mint(vm.addr(privateKey), feesAmount);
+        testUSDCContract.mint(sender, feesAmount);
         // approve fees plug to spend test USDC
         testUSDCContract.approve(address(feesPlug), feesAmount);
 
-        address sender = vm.addr(privateKey);
         console.log("Sender address:", sender);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));
// mint test USDC to sender
testUSDCContract.mint(vm.addr(privateKey), feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);
address sender = vm.addr(privateKey);
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address sender = vm.addr(privateKey);
vm.startBroadcast(privateKey);
FeesPlug feesPlug = FeesPlug(payable(vm.envAddress("ARBITRUM_FEES_PLUG")));
address appGateway = vm.envAddress("APP_GATEWAY");
TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC"));
// mint test USDC to sender
testUSDCContract.mint(sender, feesAmount);
// approve fees plug to spend test USDC
testUSDCContract.approve(address(feesPlug), feesAmount);
🤖 Prompt for AI Agents
In deprecated/script/helpers/DepositCredit.s.sol around lines 15 to 26,
vm.addr(privateKey) is computed twice; derive address once by creating a local
sender = vm.addr(privateKey) before vm.startBroadcast(privateKey) and then use
sender for minting and any other address usages instead of calling
vm.addr(privateKey) again; leave vm.startBroadcast(privateKey) unchanged so
broadcasting still uses the private key.

Comment on lines +53 to +55
| `CreditsBlocked` | `(payloadId: bytes32, consumeFrom: address, amount: uint256)` | `0xe0ce1c6e339ba1b699d262b081adbc74ddc8699c19405e3a8459940944ccd9ea` |
| `CreditsUnblocked` | `(payloadId: bytes32, consumeFrom: address)` | `0xe19214f41bd8f45a4fa569e176cdb3700de18b99f163f385cdfd210118dc7aa3` |
| `CreditsUnblockedAndAssigned` | `(payloadId: bytes32, consumeFrom: address, transmitter: address, amount: uint256)` | `0xf2fa1621e1a549c353279ffa16145321c7297cd56fd8fe4fa0d6d4b9ea09518c` |
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

🧩 Analysis chain

Verify event topic hashes match the updated signatures.

The CreditsBlocked, CreditsUnblocked, and CreditsUnblockedAndAssigned events changed from requestCount: uint40 to payloadId: bytes32. Confirm the topic hashes are correct for the new signatures.


🏁 Script executed:

#!/bin/bash
# Compute expected topic hash for CreditsBlocked event with new signature
echo -n "CreditsBlocked(bytes32,address,uint256)" | python -c "import sys; from hashlib import sha3_256; print('0x' + sha3_256(sys.stdin.read().encode()).hexdigest())"

Length of output: 66



---

🏁 Script executed:

```shell
#!/bin/bash
# Compute expected topic hashes for the remaining two events
echo "CreditsUnblocked:"
echo -n "CreditsUnblocked(bytes32,address)" | python -c "import sys; from hashlib import sha3_256; print('0x' + sha3_256(sys.stdin.read().encode()).hexdigest())"

echo "CreditsUnblockedAndAssigned:"
echo -n "CreditsUnblockedAndAssigned(bytes32,address,address,uint256)" | python -c "import sys; from hashlib import sha3_256; print('0x' + sha3_256(sys.stdin.read().encode()).hexdigest())"

Length of output: 180


Update all three event topic hashes to match the new signatures.

All event topic hashes are incorrect:

  • CreditsBlocked: should be 0xade089e13c1bf6bdd5444d99c6ba16a141ddeaaa409cff5f373edf351031780f
  • CreditsUnblocked: should be 0xcd0d8d53b493fdf455aa5e7c5e8b6520a1b50baf8c03f39ab7cdd6f2fd4494d7
  • CreditsUnblockedAndAssigned: should be 0x24c2fc23779c30f40dae52a3ddcec2a5107ead70d6b3767c7ea5e6e98cd384b8

Incorrect hashes break event filtering in smart contract indexers and monitoring systems. These must match the actual event signatures or external tools won't identify events correctly.

🤖 Prompt for AI Agents
In EventTopics.md around lines 53 to 55, the three event topic hashes are
incorrect; update the three topic hash values to the new correct hashes: set
CreditsBlocked to
0xade089e13c1bf6bdd5444d99c6ba16a141ddeaaa409cff5f373edf351031780f,
CreditsUnblocked to
0xcd0d8d53b493fdf455aa5e7c5e8b6520a1b50baf8c03f39ab7cdd6f2fd4494d7, and
CreditsUnblockedAndAssigned to
0x24c2fc23779c30f40dae52a3ddcec2a5107ead70d6b3767c7ea5e6e98cd384b8 so the
documented topic hashes match the actual event signatures used by indexers and
monitoring tools.

contractAddress: string;
data: string;
nonce: number;
nonce: number | string;
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

🧩 Analysis chain

Verify that all consumers of WatcherMultiCallParams handle both nonce types correctly.

The nonce field now accepts number | string. Ensure that all downstream code properly validates and converts string nonces where numeric operations are required.


🏁 Script executed:

#!/bin/bash
# Search for WatcherMultiCallParams usage to verify type handling
rg -nP -C5 'WatcherMultiCallParams' --type=ts

Length of output: 3582


🏁 Script executed:

#!/bin/bash
# Find signWatcherMessage implementation and params consumption
rg -nP 'signWatcherMessage|params\s*,' --type=ts -C3 | head -100

Length of output: 2250


🏁 Script executed:

#!/bin/bash
# Search for where signWatcherMessage is defined
fd -e ts | xargs rg -l 'export.*signWatcherMessage.*function|function.*signWatcherMessage' --type=ts

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where WatcherMultiCallParams params are passed/consumed
rg -nP 'await\s+\w+\(\s*params|multiCall.*params|\(params' --type=ts -C3

Length of output: 1005


🏁 Script executed:

#!/bin/bash
# View the full signWatcherMessage function implementation
cat hardhat-scripts/utils/sign.ts

Length of output: 2240


🏁 Script executed:

#!/bin/bash
# Search for setAppGatewayConfigs to see how it uses the params
rg -nP 'setAppGatewayConfigs' --type=ts -A5 -B5

Length of output: 1612


🏁 Script executed:

#!/bin/bash
# Find signWatcherMultiCallMessage return type
fd -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l 'signWatcherMultiCallMessage' | grep -E 'src/'

Length of output: 184


🏁 Script executed:

#!/bin/bash
# Search for the actual implementation in src
rg -nP 'export.*signWatcherMultiCallMessage|function signWatcherMultiCallMessage' --type=ts -A20 | head -50

Length of output: 951


🏁 Script executed:

#!/bin/bash
# Get the full signWatcherMultiCallMessage implementation to see return type
cat src/signer.ts

Length of output: 859


Fix inconsistent nonce type handling between consumers of signWatcherMessage.

The signWatcherMultiCallMessage function returns nonce as a hex string (from getNonce()). Consumers handle this inconsistently:

  • hardhat-scripts/utils/sign.ts line 62 converts to number: nonce: Number(nonce)
  • hardhat-scripts/deploy/6.connect.ts line 185 assigns without conversion: nonce: nonce

Both files call the same function and create WatcherMultiCallParams objects. The 6.connect.ts consumer should either convert to number for consistency with sign.ts, or both should remain as strings—but they must be aligned. Verify which type setAppGatewayConfigs actually expects and standardize the handling across both callsites.

🤖 Prompt for AI Agents
In hardhat-scripts/constants/types.ts around line 10, the nonce union type
(number | string) causes inconsistent handling between callers; decide on one
canonical type (prefer numeric) and enforce it across the codebase: update the
WatcherMultiCallParams nonce type to number (or string if you opt for hex) in
types.ts, modify signWatcherMessage/signWatcherMultiCallMessage to return that
canonical type (convert hex to Number if choosing number), and update both
callsites (hardhat-scripts/utils/sign.ts line ~62 and
hardhat-scripts/deploy/6.connect.ts line ~185) to pass the canonical type (use
Number(nonce) if switching to number or ensure String(nonce) if switching to
string); also verify and, if needed, update the call to setAppGatewayConfigs to
accept the chosen nonce type so all consumers are aligned.

Comment on lines 59 to 64
const params: WatcherMultiCallParams = {
contractAddress: targetContractAddress,
data: calldata,
nonce,
nonce: Number(nonce),
signature,
};
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

Stop coercing the watcher nonce to Number.
nonce comes back as a BigNumber/uint256. Casting through Number(...) silently drops precision once it grows beyond 2^53-1, so the payload you forward will no longer match the signed digest and the watcher call will start reverting. This should stay in an exact type—e.g. forward the BigNumber as-is or call nonce.toString() to satisfy the struct type.

Apply this diff:

-    nonce: Number(nonce),
+    nonce: typeof nonce === "string" ? nonce : nonce.toString(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params: WatcherMultiCallParams = {
contractAddress: targetContractAddress,
data: calldata,
nonce,
nonce: Number(nonce),
signature,
};
const params: WatcherMultiCallParams = {
contractAddress: targetContractAddress,
data: calldata,
nonce: typeof nonce === "string" ? nonce : nonce.toString(),
signature,
};
🤖 Prompt for AI Agents
In hardhat-scripts/utils/sign.ts around lines 59 to 64, the code coerces nonce
with Number(nonce) which can lose precision for BigNumber/uint256 values;
instead preserve exact value by passing the BigNumber through unchanged or call
nonce.toString() if the WatcherMultiCallParams struct expects a string; update
the params object to use the original BigNumber (or nonce.toString()) for nonce
so the forwarded payload matches the signed digest.

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Counter} from "../../test/apps/counter/Counter.sol";
import {toBytes32Format} from "../../../../contracts/utils/common/Converters.sol";
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

Fix incorrect import path.

The import path ../../../../contracts/utils/common/Converters.sol goes up 4 directories from script/counter/, which exceeds the repository root. This should be ../../contracts/utils/common/Converters.sol (2 levels up).

Apply this diff:

-import {toBytes32Format} from "../../../../contracts/utils/common/Converters.sol";
+import {toBytes32Format} from "../../contracts/utils/common/Converters.sol";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {toBytes32Format} from "../../../../contracts/utils/common/Converters.sol";
import {toBytes32Format} from "../../contracts/utils/common/Converters.sol";
🤖 Prompt for AI Agents
In script/counter/DeployCounterPlug.s.sol around line 7, the import path is
incorrect (goes up 4 dirs past repo root); change the import from
"../../../../contracts/utils/common/Converters.sol" to
"../../contracts/utils/common/Converters.sol" so it goes up two levels from
script/counter to reach contracts/utils/common/Converters.sol and update the
single import statement accordingly.

Comment on lines +20 to +21
Counter counter = new Counter();
counter.initSocket(toBytes32Format(appGateway), socket, 1);
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

Document or make the hardcoded value configurable.

The hardcoded value 1 passed to initSocket lacks context. Add a comment explaining what this parameter represents (e.g., chain ID, version) or make it configurable via environment variable if it may vary across deployments.

Example:

+        // Chain identifier for the socket connection
         Counter counter = new Counter();
         counter.initSocket(toBytes32Format(appGateway), socket, 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Counter counter = new Counter();
counter.initSocket(toBytes32Format(appGateway), socket, 1);
// Chain identifier for the socket connection
Counter counter = new Counter();
counter.initSocket(toBytes32Format(appGateway), socket, 1);
🤖 Prompt for AI Agents
In script/counter/DeployCounterPlug.s.sol around lines 20 to 21, the literal 1
passed to counter.initSocket lacks context; update the code to either (a)
replace the hardcoded literal with a named constant or constructor/config
variable (e.g., uint16 CHAIN_ID or SOCKET_VERSION) declared at the top of the
file, or (b) read the value from an environment/configuration source and pass
that variable instead, and add a one-line comment explaining what the parameter
represents (for example "chain ID for destination network" or "socket protocol
version") so future maintainers know its intent.

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: 11

Caution

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

⚠️ Outside diff range comments (2)
contracts/evmx/fees/Credit.sol (2)

16-20: Add missing struct and library imports

RawPayload, Transaction, OverrideParams, and OverrideParamsLib are referenced below but never imported, so this file will not compile. Add the missing imports next to the other includes.

+import {OverrideParams, RawPayload, Transaction} from "../../utils/common/Structs.sol";
+import "../../utils/OverrideParamsLib.sol";

264-282: Fix invalid _createRequest implementation

This block will not compile: Solidity has no async keyword, overrideParams is never declared, and the user-supplied maxFees_ argument is ignored (everything is forced to getMaxFees(chainSlug_)). Declare a local OverrideParams, populate it (while honoring the caller’s maxFees_ as appropriate), and drop the stray keyword.

-    ) internal async {
-        overrideParams = overrideParams.setMaxFees(getMaxFees(chainSlug_)).setConsumeFrom(
-            consumeFrom_
-        );
-
-        RawPayload memory rawPayload;
-        rawPayload.overrideParams = overrideParams;
+    ) internal {
+        OverrideParams memory params;
+        params = OverrideParamsLib
+            .setMaxFees(params, maxFees_)
+            .setConsumeFrom(consumeFrom_);
+
+        RawPayload memory rawPayload;
+        rawPayload.overrideParams = params;
         rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
         watcher__().addPayloadData(rawPayload, address(this));
     }

If you still need to clamp against getMaxFees(chainSlug_), do it explicitly before handing the value to setMaxFees. This should restore both compilation and the intended fee behaviour.

♻️ Duplicate comments (3)
contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)

8-11: Import RawPayload so this file compiles.

RawPayload is used in the event signature and function arguments, but the Structs import only pulls in DigestParams and Payload. Add it to the import list to avoid Identifier not found or not unique errors.

-import {DigestParams, Payload} from "../../../utils/common/Structs.sol";
+import {DigestParams, Payload, RawPayload} from "../../../utils/common/Structs.sol";

149-187: Honor caller-provided gasLimit.

The current _validate leaves gasLimit at its zero default whenever an override is supplied, so every payload digests with gasLimit == 0. Initialize gasLimit from rawPayload_.overrideParams.gasLimit, and only compute defaults when it’s still zero.

-    ) internal view returns (uint256 gasLimit) {
-        if (
+    ) internal view returns (uint256 gasLimit) {
+        gasLimit = rawPayload_.overrideParams.gasLimit;
+        if (
             rawPayload_.overrideParams.value >
             chainMaxMsgValueLimit[rawPayload_.transaction.chainSlug]
         ) revert MaxMsgValueLimitExceeded();
@@
-        if (rawPayload_.overrideParams.gasLimit == 0) {
+        if (gasLimit == 0) {
             if (rawPayload_.transaction.chainSlug == 5000) {
                 // Mantle default gas limit
                 gasLimit = 8_000_000_000;
contracts/evmx/fees/Credit.sol (1)

226-227: Stop clobbering allowances for the watcher

This path still overwrites whatever approval the user previously set when the watcher calls transferFrom. We already flagged this earlier; it breaks infinite approvals and surprises integrators. Let the watcher bypass allowances by transferring directly.

-        if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
-        return super.transferFrom(from_, to_, amount_);
+        if (msg.sender == address(watcher__())) {
+            _transfer(from_, to_, amount_);
+            return true;
+        }
+        return super.transferFrom(from_, to_, amount_);
🧹 Nitpick comments (5)
test/apps/counter/Counter.sol (2)

20-22: Redundant getter: counter is public.

Since counter is declared public at line 12, Solidity automatically generates a counter() getter. This getCounter() function duplicates that functionality.

Remove this function and use the auto-generated counter() getter, or make the state variable private if you prefer the explicit getter name.


24-27: Comment references non-existent parameter.

Line 25's comment mentions params_ but this parameter doesn't exist in the function signature.

Update the comment to reflect the actual signature:

-        // can set overrides here: _setOverrides(params_);
+        // can set overrides here if needed: _setOverrides(overrides_);

Or remove the comment if overrides aren't currently supported.

test/apps/Counter.t.sol (2)

23-32: deployCounter overwrites shared state variable.

The counter state variable (line 10) is overwritten each time deployCounter() is called. In tests like testCounterIncrementMultipleChains (lines 62-63), two counters are deployed, but only the last one remains accessible via counter.

While tests correctly retrieve counter addresses via getOnChainAndForwarderAddresses() and don't rely on the state variable for assertions, the shared state is confusing and error-prone.

Consider either:

  1. Make deployCounter() return the Counter instance instead of storing in state:
-    function deployCounter(uint32 chainSlug) internal {
-        counter = new Counter();
+    function deployCounter(uint32 chainSlug) internal returns (Counter) {
+        Counter newCounter = new Counter();
-        counter.initSocket(
+        newCounter.initSocket(
             toBytes32Format(address(counterGateway)),
             address(getSocketConfig(chainSlug).socket),
             getSocketConfig(chainSlug).switchboard.switchboardId()
         );
-        setupGatewayAndPlugs(chainSlug, address(counterGateway), toBytes32Format(address(counter)));
-        counterGateway.uploadPlug(chainSlug, counterId, toBytes32Format(address(counter)));
+        setupGatewayAndPlugs(chainSlug, address(counterGateway), toBytes32Format(address(newCounter)));
+        counterGateway.uploadPlug(chainSlug, counterId, toBytes32Format(address(newCounter)));
+        return newCounter;
     }
  1. Or use a mapping mapping(uint32 => Counter) counters to track multiple deployed counters per chain.

94-106: Test has no assertions about read results.

This test deploys counters and calls readCounters() but doesn't verify the read operation's outcome. It only verifies that executePayload() completes without reverting.

Add assertions to verify the read operation succeeded or the expected state was reached:

counterGateway.readCounters(arbCounterForwarder);
executePayload();

// Add assertions here to verify read results
// e.g., check that counterGateway received the correct counter values

Or add a comment explaining this is an integration test that only verifies execution completes.

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

41-44: Validate array lengths before looping
registerSiblings walks chainSlugs_ but reads siblingPlugs_[i] without checking the arrays are the same size. Any mismatch reverts with a cryptic panic and wastes gas. This should enforce chainSlugs_.length == siblingPlugs_.length before the loop so the revert reason is explicit.

     function registerSiblings(uint32[] memory chainSlugs_, address[] memory siblingPlugs_) public {
-        for (uint256 i = 0; i < chainSlugs_.length; i++) {
+        if (chainSlugs_.length != siblingPlugs_.length) revert("length mismatch");
+        for (uint256 i = 0; i < chainSlugs_.length; i++) {
             registerSibling(chainSlugs_[i], siblingPlugs_[i]);
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba5a850 and 053fd3c.

📒 Files selected for processing (24)
  • contracts/evmx/fees/Credit.sol (6 hunks)
  • contracts/evmx/fees/MessageResolver.sol (1 hunks)
  • contracts/evmx/interfaces/IFeesManager.sol (2 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (5 hunks)
  • contracts/protocol/Socket.sol (4 hunks)
  • contracts/protocol/SocketConfig.sol (4 hunks)
  • contracts/protocol/SocketUtils.sol (1 hunks)
  • contracts/protocol/base/MessagePlugBase.sol (2 hunks)
  • contracts/protocol/base/PlugBase.sol (1 hunks)
  • contracts/protocol/interfaces/ISocket.sol (4 hunks)
  • contracts/protocol/interfaces/ISwitchboard.sol (2 hunks)
  • contracts/protocol/switchboard/FastSwitchboard.sol (3 hunks)
  • contracts/protocol/switchboard/MessageSwitchboard.sol (9 hunks)
  • contracts/protocol/switchboard/SwitchboardBase.sol (2 hunks)
  • contracts/utils/common/AccessRoles.sol (1 hunks)
  • contracts/utils/common/Constants.sol (0 hunks)
  • contracts/utils/common/Structs.sol (5 hunks)
  • foundry.toml (1 hunks)
  • test/SetupTest.t.sol (21 hunks)
  • test/Utils.t.sol (1 hunks)
  • test/apps/Counter.t.sol (3 hunks)
  • test/apps/counter/Counter.sol (1 hunks)
  • test/mocks/MockPlug.sol (1 hunks)
  • test/switchboard/MessageSwitchboard.t.sol (1 hunks)
💤 Files with no reviewable changes (1)
  • contracts/utils/common/Constants.sol
🔇 Additional comments (2)
foundry.toml (2)

10-10: Verify via_ir=true is intentional and doesn't affect contract behavior.

Enabling IR compilation is a significant compiler change that can alter bytecode optimization and potentially introduce subtle behavior differences. This should be tested against the old compilation path to ensure no regressions.

Has this been tested to confirm contract behavior is identical with/without via_ir? Are there known compiler differences for this Solidity version (0.8.22)?


13-28: Verify all contract addresses are correct for Phase 1 deployment.

All 16 contract labels have been remapped to new addresses. Incorrect addresses here will cause Forge verification and debugging to fail silently against wrong contracts.

Please confirm:

  • These addresses correspond to the Phase 1 contract deployments
  • They've been tested with current contract implementations
  • If these are testnet/staging addresses, document the deployment context

Comment on lines 181 to 186
address plugAddress_,
bytes memory extraData_
) external view returns (uint64 switchboardId, bytes memory configData) {
switchboardId = plugSwitchboardIds[plugAddress_];
configData = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(plugAddress_, extraData_);
}
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

Guard getPlugConfig for disconnected plugs.
When plugSwitchboardIds[plugAddress_] is zero this now jumps through switchboardAddresses[0] (address(0)), so external callers get a generic “call to non-contract” revert instead of the intended PlugNotConnected() signal. This should detect the zero id and revert explicitly before touching the switchboard.

     ) external view returns (uint64 switchboardId, bytes memory configData) {
         switchboardId = plugSwitchboardIds[plugAddress_];
+        if (switchboardId == 0) revert PlugNotConnected();
-        configData = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(
-            plugAddress_,
-            extraData_
-        );
+        address switchboard = switchboardAddresses[switchboardId];
+        configData = ISwitchboard(switchboard).getPlugConfig(plugAddress_, extraData_);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
address plugAddress_,
bytes memory extraData_
) external view returns (uint64 switchboardId, bytes memory configData) {
switchboardId = plugSwitchboardIds[plugAddress_];
configData = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(plugAddress_, extraData_);
}
address plugAddress_,
bytes memory extraData_
) external view returns (uint64 switchboardId, bytes memory configData) {
switchboardId = plugSwitchboardIds[plugAddress_];
if (switchboardId == 0) revert PlugNotConnected();
address switchboard = switchboardAddresses[switchboardId];
configData = ISwitchboard(switchboard).getPlugConfig(plugAddress_, extraData_);
}
🤖 Prompt for AI Agents
In contracts/protocol/SocketConfig.sol around lines 181 to 186, the function
reads plugSwitchboardIds[plugAddress_] and immediately uses it to index
switchboardAddresses, which when zero causes a call to address(0) and yields a
generic revert; instead detect when switchboardId == 0 and revert with the
intended PlugNotConnected() before any lookup/call. Modify the flow to read
switchboardId, if it's zero revert with PlugNotConnected(), then fetch
switchboardAddresses[switchboardId] and call getPlugConfig as before.

Comment on lines +67 to 71
function allowPayload(bytes32 digest_, bytes32, address target_, bytes memory source_ ) external view returns (bool) {
(bytes32 appGatewayId) = abi.decode(source_, (bytes32));
if (plugAppGatewayIds[target_] != appGatewayId) revert InvalidSource();
return isAttested[digest_];
}
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

Decode full source tuple before comparing
allowPayload now decodes source_ as a single bytes32, but the interface doc (and existing producers) encode (chainSlug, plug) for this field. Decoding as a lone word will revert when extra bytes are present, blocking every execution attempt. This should decode the same tuple that writers emit—then derive the app gateway (e.g., via stored mapping) from the plug rather than assuming the payload already passed a bytes32 id.citeturn0file3

🤖 Prompt for AI Agents
In contracts/protocol/switchboard/FastSwitchboard.sol around lines 67–71, the
function currently decodes source_ as a single bytes32 which will revert if
writers encoded a tuple; instead decode the full tuple the producers emit (e.g.,
(bytes32 chainSlug, address plug_)), then derive the app gateway id from the
plug by looking it up in the existing mapping (e.g., bytes32 appGatewayId =
plugAppGatewayIds[plug_]) and compare that to plugAppGatewayIds[target_] before
returning isAttested[digest_]; update the decode and the lookup/compare
accordingly so extra bytes no longer cause a revert.

Comment on lines +95 to +100
function updatePlugConfig(address plug_, bytes memory configData_) external virtual {
(bytes32 appGatewayId_) = abi.decode(configData_, ( bytes32));
plugAppGatewayIds[plug_] = appGatewayId_;
emit PlugConfigUpdated(plug_, appGatewayId_);
}

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

Config decode breaks sibling registration
updatePlugConfig assumes configData_ is abi.encode(bytes32 appGatewayId_), but MessagePlugBase.registerSibling now sends abi.encode(chainSlug_, toBytes32Format(siblingPlug_)). The decode therefore reverts (or misinterprets the first slot as the app gateway), leaving plugAppGatewayIds unset and causing allowPayload to throw InvalidSource for every payload. This should decode the same struct that the plug encodes (chain slug + sibling) or, alternatively, update the plug to send only the bytes32 app gateway id so both sides agree.citeturn0file4turn0file1

🤖 Prompt for AI Agents
In contracts/protocol/switchboard/FastSwitchboard.sol around lines 95-100,
updatePlugConfig currently decodes configData_ as a single bytes32 but
registerSibling encodes (chainSlug, sibling) as two bytes32 values; change the
decode to abi.decode(configData_, (bytes32, bytes32)), assign the second decoded
value (the sibling/app gateway id) to plugAppGatewayIds[plug_], and emit
PlugConfigUpdated with that value (or alternatively keep this file unchanged and
modify registerSibling to send only the bytes32 app gateway id) so both sides
encode/decode the same payload.

Comment on lines +638 to +640
* @notice Event emitted when plug configuration is updated
*/
event PlugConfigUpdated(address indexed plug, bytes configData);
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

Remove duplicate PlugConfigUpdated event

There are two PlugConfigUpdated events in this file with different signatures (the other is declared around Line 105). Solidity disallows duplicate event names, so this will break compilation. Drop the second definition or rename it to something else; otherwise this file will not build.

-    event PlugConfigUpdated(address indexed plug, bytes configData);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @notice Event emitted when plug configuration is updated
*/
event PlugConfigUpdated(address indexed plug, bytes configData);
* @notice Event emitted when plug configuration is updated
*/
Suggested change
* @notice Event emitted when plug configuration is updated
*/
event PlugConfigUpdated(address indexed plug, bytes configData);
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/MessageSwitchboard.sol around lines 638 to
640, there is a duplicate declaration of the PlugConfigUpdated event (the
original is declared around line 105) which will break compilation; remove this
second event declaration or rename it to a unique event name and update all
references/usages accordingly so there are no duplicate event names in the
contract.

Comment on lines +15 to +18
function increase() external onlySocket {
counter++;
emit CounterIncreased(counter);
}
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

Interface mismatch: Gateway sends value but increase() ignores it.

Line 24-26 shows increaseOnGateway(uint256 value_) calls ICounterAppGateway.increase(value_), which passes a value parameter. However, the increase() function here takes no parameters and simply increments by 1, ignoring any value sent from the gateway.

This creates a semantic mismatch where the caller thinks they can specify an increment amount, but the implementation always increments by 1.

Either:

  1. Remove the value_ parameter from ICounterAppGateway.increase() if incrementing by 1 is the intended behavior, or
  2. Update increase() to accept and use the value parameter:
-    function increase() external onlySocket {
-        counter++;
+    function increase(uint256 value_) external onlySocket {
+        counter += value_;
         emit CounterIncreased(counter);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function increase() external onlySocket {
counter++;
emit CounterIncreased(counter);
}
function increase(uint256 value_) external onlySocket {
counter += value_;
emit CounterIncreased(counter);
}

Comment on lines +708 to 731
function _resolvePayload(PromiseReturnData memory promiseReturnData) internal {
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, feesAmount),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, feesAmount))
});
watcherNonce++;
watcher.resolvePayload(params);
}

function _markRevert(
PromiseReturnData memory promiseReturnData,
bool isRevertingOnchain_
) internal {
watcherMultiCall(
address(promiseResolver),
abi.encodeWithSelector(
PromiseResolver.markRevert.selector,
promiseReturnData,
isRevertingOnchain_
)
);
}

function setupGatewayAndPlugs(
uint32 chainSlug_,
IAppGateway appGateway_,
bytes32[] memory contractIds_
) internal {
// Count valid plugs first. In some cases we might have contractIds such that oly a subset is
// deployed on a chain. for ex, vault on source, and supertoken on destination.
uint256 validPlugCount = _countValidPlugs(appGateway_, contractIds_, chainSlug_);

// Create array with exact size needed
AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount);
_populateConfigs(configs, appGateway_, contractIds_, chainSlug_);

// Only call watcher if we have valid configs
if (validPlugCount > 0) {
watcherMultiCall(
address(configurations),
abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs)
);
}
}

function _countValidPlugs(
IAppGateway appGateway_,
bytes32[] memory contractIds_,
uint32 chainSlug_
) internal view returns (uint256 validCount) {
for (uint i = 0; i < contractIds_.length; i++) {
bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_);
if (plug != bytes32(0)) {
validCount++;
}
}
}

function _populateConfigs(
AppGatewayConfig[] memory configs,
IAppGateway appGateway_,
bytes32[] memory contractIds_,
uint32 chainSlug_
) internal view {
uint256 configIndex = 0;
uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType());

for (uint i = 0; i < contractIds_.length; i++) {
bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_);
if (plug != bytes32(0)) {
configs[configIndex] = AppGatewayConfig({
plug: plug,
chainSlug: chainSlug_,
plugConfig: PlugConfigGeneric({
appGatewayId: toBytes32Format(address(appGateway_)),
switchboardId: switchboardId
})
});
configIndex++;
}
}
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, isRevertingOnchain_),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, isRevertingOnchain_))
});
watcherNonce++;
watcher.markRevert(params);
}
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

Restore watcher impersonation before resolve/markRevert.
Watcher.resolvePayload and Watcher.markRevert are still onlyWatcher. Without hoax(watcherEOA) these helpers revert immediately when executePayload hits a success or failure branch, so none of the tests can advance payload state. This should prank as the watcher before calling both functions.

     WatcherMultiCallParams memory params = WatcherMultiCallParams({
         contractAddress: address(watcher),
         data: abi.encode(promiseReturnData, feesAmount),
         nonce: watcherNonce,
         signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, feesAmount))
     });
     watcherNonce++;
+    hoax(watcherEOA);
     watcher.resolvePayload(params);
 }
 
 function _markRevert(
     PromiseReturnData memory promiseReturnData,
     bool isRevertingOnchain_
 ) internal {
     WatcherMultiCallParams memory params = WatcherMultiCallParams({
         contractAddress: address(watcher),
         data: abi.encode(promiseReturnData, isRevertingOnchain_),
         nonce: watcherNonce,
         signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, isRevertingOnchain_))
     });
     watcherNonce++;
+    hoax(watcherEOA);
     watcher.markRevert(params);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _resolvePayload(PromiseReturnData memory promiseReturnData) internal {
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, feesAmount),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, feesAmount))
});
watcherNonce++;
watcher.resolvePayload(params);
}
function _markRevert(
PromiseReturnData memory promiseReturnData,
bool isRevertingOnchain_
) internal {
watcherMultiCall(
address(promiseResolver),
abi.encodeWithSelector(
PromiseResolver.markRevert.selector,
promiseReturnData,
isRevertingOnchain_
)
);
}
function setupGatewayAndPlugs(
uint32 chainSlug_,
IAppGateway appGateway_,
bytes32[] memory contractIds_
) internal {
// Count valid plugs first. In some cases we might have contractIds such that oly a subset is
// deployed on a chain. for ex, vault on source, and supertoken on destination.
uint256 validPlugCount = _countValidPlugs(appGateway_, contractIds_, chainSlug_);
// Create array with exact size needed
AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount);
_populateConfigs(configs, appGateway_, contractIds_, chainSlug_);
// Only call watcher if we have valid configs
if (validPlugCount > 0) {
watcherMultiCall(
address(configurations),
abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs)
);
}
}
function _countValidPlugs(
IAppGateway appGateway_,
bytes32[] memory contractIds_,
uint32 chainSlug_
) internal view returns (uint256 validCount) {
for (uint i = 0; i < contractIds_.length; i++) {
bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_);
if (plug != bytes32(0)) {
validCount++;
}
}
}
function _populateConfigs(
AppGatewayConfig[] memory configs,
IAppGateway appGateway_,
bytes32[] memory contractIds_,
uint32 chainSlug_
) internal view {
uint256 configIndex = 0;
uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType());
for (uint i = 0; i < contractIds_.length; i++) {
bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_);
if (plug != bytes32(0)) {
configs[configIndex] = AppGatewayConfig({
plug: plug,
chainSlug: chainSlug_,
plugConfig: PlugConfigGeneric({
appGatewayId: toBytes32Format(address(appGateway_)),
switchboardId: switchboardId
})
});
configIndex++;
}
}
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, isRevertingOnchain_),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, isRevertingOnchain_))
});
watcherNonce++;
watcher.markRevert(params);
}
function _resolvePayload(PromiseReturnData memory promiseReturnData) internal {
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, feesAmount),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, feesAmount))
});
watcherNonce++;
hoax(watcherEOA);
watcher.resolvePayload(params);
}
function _markRevert(
PromiseReturnData memory promiseReturnData,
bool isRevertingOnchain_
) internal {
WatcherMultiCallParams memory params = WatcherMultiCallParams({
contractAddress: address(watcher),
data: abi.encode(promiseReturnData, isRevertingOnchain_),
nonce: watcherNonce,
signature: _createWatcherSignature(address(watcher), abi.encode(promiseReturnData, isRevertingOnchain_))
});
watcherNonce++;
hoax(watcherEOA);
watcher.markRevert(params);
}
🤖 Prompt for AI Agents
In test/SetupTest.t.sol around lines 708 to 731, the helpers _resolvePayload and
_markRevert call watcher.resolvePayload/markRevert while still impersonating the
test caller, causing onlyWatcher checks to fail; before invoking
watcher.resolvePayload and watcher.markRevert, restore watcher impersonation by
calling hoax(watcherEOA) (or vm.prank(watcherEOA) if preferred) so the call
comes from the watcher EOA, then proceed to increment watcherNonce and call the
watcher methods as before.

Comment on lines +315 to +319
function _createWatcherSignature(bytes32 payloadId) internal view returns (bytes memory) {
// markRefundEligible signs: keccak256(abi.encodePacked(switchboardAddress, chainSlug, payloadId))
bytes32 digest = keccak256(abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, payloadId));
return createSignature(digest, watcherPrivateKey);
}
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

Align _createWatcherSignature mutability with createSignature.

Once createSignature stops being pure, this wrapper can no longer remain view without causing a secondary compile failure. Remove the view modifier here (and rely on the outer function being internal) so the call is valid.

-    ) internal view returns (bytes memory) {
+    ) internal returns (bytes memory) {
🤖 Prompt for AI Agents
In test/switchboard/MessageSwitchboard.t.sol around lines 315 to 319, the helper
`_createWatcherSignature` is declared `internal view` but calls
`createSignature`, which is no longer `pure` and therefore cannot be invoked
from a `view` function; remove the `view` modifier so the function is simply
`internal` (keep the same return type and body) to make the call valid.

Comment on lines +10 to 22
function createSignature(
bytes32 digest_,
uint256 privateKey_
) public pure returns (bytes memory sig) {
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_));
(uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest);
sig = new bytes(65);
bytes1 v32 = bytes1(sigV);
assembly {
mstore(add(sig, 96), v32)
mstore(add(sig, 32), sigR)
mstore(add(sig, 64), sigS)
}
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

Remove the pure modifier from createSignature.

vm.sign is a cheatcode call, so keeping this helper pure makes the compiler emit TypeError: Function declared as pure, but this expression (potentially) reads from the environment or state. Drop the pure mutability (or move the helper into a non-pure harness) so the file compiles.

-    ) public pure returns (bytes memory sig) {
+    ) public returns (bytes memory sig) {
🤖 Prompt for AI Agents
In test/Utils.t.sol around lines 10 to 22, the createSignature function is
declared pure but calls the vm.sign cheatcode which reads from the environment;
remove the pure modifier from the function declaration (make it just public
returns (bytes memory) or another non-pure mutability) so the compiler no longer
errors, ensuring the rest of the function body remains unchanged.

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 (2)
deprecated/test/mock/MockFastSwitchboard.sol (1)

34-55: Bring MockFastSwitchboard back in sync with ISwitchboard.

This should match the new payload-first interface: allowPayload still uses the legacy (bytes32, bytes32) signature, processPayload keeps the removed triggerId_ argument and never returns the required payloadId, and the new increaseFeesForPayload, updatePlugConfig, and getPlugConfig overrides are missing. The mock fails to compile as soon as solc checks the overrides. Patch it like below (or equivalent) to restore compatibility.

-    function allowPayload(bytes32, bytes32) external view returns (bool) {
+    function allowPayload(
+        bytes32,
+        bytes32,
+        address,
+        bytes memory
+    ) external view returns (bool) {
         // digest has enough attestations
         return isPayloadAllowed;
     }

-    function processPayload(
-        address plug_,
-        bytes32 triggerId_,
-        bytes calldata payload_,
-        bytes calldata overrides_
-    ) external payable override {
-        // Simple implementation that just accepts the trigger
-        // In a real switchboard, this would process the trigger
-    }
+    function processPayload(
+        address,
+        bytes calldata,
+        bytes calldata
+    ) external payable override returns (bytes32 payloadId_) {
+        payloadId_ = bytes32(0);
+    }
+
+    function increaseFeesForPayload(
+        bytes32,
+        address,
+        bytes calldata
+    ) external payable override {}
+
+    function updatePlugConfig(address, bytes memory) external override {}
+
+    function getPlugConfig(address, bytes memory) external view override returns (bytes memory) {
+        return bytes("");
+    }
deprecated/test/protocol/switchboards/FastSwitchboardTest.t.sol (1)

24-33: Sync the test override with the new FastSwitchboard signature.

This should mirror FastSwitchboard.processPayload(address,bytes,bytes) returns (bytes32), but the override still accepts triggerId_ and never returns a payloadId. After the interface change it no longer overrides anything, so both this definition and the call below that still passes a triggerId fail to compile. Update the signature (and the emitted data) to the payloadId-based shape and return the payloadId produced by the base implementation or an appropriate stub.

♻️ Duplicate comments (14)
deprecated/AuctionManager.sol (2)

17-21: Past concerns remain unresolved.

The previous review identified three issues with this struct that are still present:

  1. It's defined in deprecated/ but should be in contracts/utils/common/Structs.sol (where IAuctionManager expects to import it from)
  2. Missing NatSpec documentation
  3. Unbounded extraData field allows arbitrarily large inputs, causing unbounded gas costs at lines 133 (hashing) and 146 (storage)

These issues should be addressed before merging.


236-236: Explicit type casting is fine but doesn't address core issues.

Using bytes("") instead of "" makes the type explicit, which is acceptable. However, this doesn't address the unbounded extraData concern raised in previous reviews—the field still lacks size validation throughout the contract.

contracts/protocol/switchboard/MessageSwitchboard.sol (1)

663-666: Remove the duplicate PlugConfigUpdated declaration.

This should be declared only once; the file already defines event PlugConfigUpdated(address indexed plug, uint32 indexed chainSlug, bytes32 siblingPlug); earlier. Solidity rejects duplicate event names, so keeping this second declaration breaks compilation. Delete the redundant event.

-    /**
-     * @notice Event emitted when plug configuration is updated
-     */
-    event PlugConfigUpdated(address indexed plug, bytes configData);
contracts/evmx/interfaces/IConfigurations.sol (1)

48-48: Use calldata for external struct param to avoid unnecessary copies.

The past review comment remains valid: this should be calldata for gas efficiency and ABI consistency, as interfaces constrain implementers.

Based on past review.

Apply this diff:

-    function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external;
+    function setAppGatewayConfigs(WatcherMultiCallParams calldata params_) external;
deprecated/test/evmx/Watcher.t.sol (1)

262-283: Fix variable naming collision with type.

Using RawPayload as both a type name and variable name collides and hurts readability. Rename the variable to lowercase camelCase (e.g., rawPayloads).

Based on past review.

Apply this diff:

-        RawPayload[] memory RawPayload = new RawPayload[](0);
+        RawPayload[] memory rawPayloads = new RawPayload[](0);
 
         hoax(watcherAddress);
         requestHandler.submitRequest(
             1000,
             address(0x1),
             address(0x2),
             address(0x3),
-            RawPayload,
+            rawPayloads,
             ""
         );
 
         hoax(nonOwner);
         vm.expectRevert(abi.encodeWithSelector(OnlyWatcherAllowed.selector));
         requestHandler.submitRequest(
             1000,
             address(0x1),
             address(0x2),
             address(0x3),
-            RawPayload,
+            rawPayloads,
             ""
         );
contracts/protocol/base/MessagePlugBase.sol (1)

26-29: Fix config encoding mismatch with FastSwitchboard.

The past review comment remains valid: updatePlugConfig now encodes (chainSlug_, toBytes32Format(siblingPlug_)), but if FastSwitchboard.updatePlugConfig expects only a single bytes32 appGatewayId, this will cause decode failures or silent misregistration.

Based on past review.

Verify the expected encoding format in FastSwitchboard and adjust accordingly. If FastSwitchboard expects only the appGatewayId:

-        socket__.updatePlugConfig(abi.encode(chainSlug_, toBytes32Format(siblingPlug_)));
+        socket__.updatePlugConfig(abi.encode(toBytes32Format(siblingPlug_)));

Run this script to check FastSwitchboard's updatePlugConfig signature and decode expectations:

#!/bin/bash
# Find FastSwitchboard implementation and check updatePlugConfig decoding
ast-grep --pattern $'function updatePlugConfig($$$) {
  $$$
}'

# Check what parameters are decoded from the payload
rg -nP -A10 'function updatePlugConfig' --type=sol
contracts/protocol/switchboard/FastSwitchboard.sol (1)

89-92: Fix source decode before comparing appGatewayId

This should decode the same (uint32 srcChainSlug, bytes32 appGatewayId) tuple that writers put into executeParams.source. Right now abi.decode(source_, (bytes32)) reads only the first word (the encoded chain slug), so plugAppGatewayIds[target_] != appGatewayId will revert InvalidSource() even when the payload carries the correct app gateway id. Decode the full tuple (and optionally validate the chain slug) before checking the stored mapping.

-        (bytes32 appGatewayId) = abi.decode(source_, (bytes32));
-        if (plugAppGatewayIds[target_] != appGatewayId) revert InvalidSource();
+        (, bytes32 appGatewayId) = abi.decode(source_, (uint32, bytes32));
+        if (plugAppGatewayIds[target_] != appGatewayId) revert InvalidSource();
test/SetupTest.t.sol (1)

710-733: Verify watcher authorization for resolvePayload and markRevert.

The helpers _resolvePayload and _markRevert call watcher.resolvePayload and watcher.markRevert, which may require onlyWatcher access. If these functions are access-controlled, add watcher impersonation before the calls:

     });
     watcherNonce++;
+    hoax(watcherEOA);
     watcher.resolvePayload(params);

And similarly for _markRevert. Past comments suggest this was addressed, but verify the current implementation doesn't revert on access control checks.

contracts/evmx/watcher/Watcher.sol (3)

86-95: Credits blocked at maxFees instead of actual fees.

The current flow blocks payloadData.overrideParams.maxFees (line 89) before computing the actual fees via precompile.handlePayload (lines 93-94). This means the full maximum amount is reserved, even if the actual fee is lower.

Past review comments suggest blocking only the computed fees. If this is the intended behavior (reserve max upfront, adjust later), document it clearly. Otherwise, move blockCredits after line 95 and block the computed fees instead:

-        feesManager__().blockCredits(
-            currentPayloadId,
-            payloadData.overrideParams.consumeFrom,
-            payloadData.overrideParams.maxFees
-        );
         asyncPromise = latestAsyncPromise;

         (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile(precompile)
             .handlePayload(payloadData, latestAppGateway, currentPayloadId);
         if (fees > payloadData.overrideParams.maxFees) revert InsufficientFees();
+        feesManager__().blockCredits(
+            currentPayloadId,
+            payloadData.overrideParams.consumeFrom,
+            fees
+        );

248-256: Cancellation charges full maxFees.

cancelExecution settles with r.maxFees (line 254), charging the full reserved amount even though the payload was never executed. This should settle with 0 (nothing used) or track actual fees consumed:

-        _settlePayload(payloadId_, r.maxFees);
+        _settlePayload(payloadId_, 0);

If fees were consumed during partial execution, track them in the Payload struct and use that value.


258-261: feesUsed_ parameter ignored in settlement.

The function accepts feesUsed_ but doesn't pass it to unblockAndAssignCredits. This means the FeesManager doesn't know how much to charge vs. refund. Update the call to pass fees through:

-    feesManager__().unblockAndAssignCredits(payloadId_, transmitter);
+    feesManager__().unblockAndAssignCredits(payloadId_, transmitter, feesUsed_);

Verify the unblockAndAssignCredits signature supports a third fees parameter, updating the interface if needed.

test/switchboard/MessageSwitchboard.t.sol (2)

93-95: Remove pure modifier from getWatcherAddress.

This function calls vm.addr, which is a cheatcode that isn't pure. This will cause a compilation error. Remove the pure modifier:

-    function getWatcherAddress() public pure returns (address) {
+    function getWatcherAddress() public view returns (address) {

294-298: Align _createWatcherSignature mutability.

Once createSignature is no longer pure (it uses vm.sign), this wrapper can't remain view. Remove the view modifier:

-    ) internal view returns (bytes memory) {
+    ) internal returns (bytes memory) {
contracts/protocol/SocketConfig.sol (1)

180-186: Add zero ID guard in getPlugConfig.

When plugSwitchboardIds[plugAddress_] is zero (plug not connected), the function attempts to call switchboardAddresses[0] (address(0)), resulting in a generic "call to non-contract" revert instead of the intended PlugNotConnected() error. Add explicit validation:

     ) external view returns (uint32 switchboardId, bytes memory configData) {
         switchboardId = plugSwitchboardIds[plugAddress_];
+        if (switchboardId == 0) revert PlugNotConnected();
         configData = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(plugAddress_, extraData_);
     }
🧹 Nitpick comments (4)
PAYLOAD_ID_ARCHITECTURE.md (2)

9-11: Add language identifier to fenced code block.

The bit layout diagram should specify a language (or use text) to satisfy markdown linting and improve rendering.

Apply this diff:

-```
+```text
 [Origin: 64 bits][Verification: 64 bits][Pointer: 64 bits][Reserved: 64 bits]

---

`24-24`: **Consider standardizing "off-chain" spelling for consistency.**

Lines 24, 36, and 120 use "offchain" which static analysis flags. Consider using "off-chain" (hyphenated) for better readability and consistency with technical writing conventions.




Also applies to: 36-36, 120-120

</blockquote></details>
<details>
<summary>contracts/protocol/base/MessagePlugBase.sol (1)</summary><blockquote>

`31-35`: **Add array length validation to batch helper.**

The `registerSiblings` function should validate that `chainSlugs_` and `siblingPlugs_` have matching lengths to prevent silent partial registration or out-of-bounds access.



Apply this diff:

```diff
 function registerSiblings(uint32[] memory chainSlugs_, address[] memory siblingPlugs_) public {
+    require(chainSlugs_.length == siblingPlugs_.length, "Array length mismatch");
     for (uint256 i = 0; i < chainSlugs_.length; i++) {
         registerSibling(chainSlugs_[i], siblingPlugs_[i]);
     }
 }
contracts/protocol/SocketBatcher.sol (1)

66-102: Remove commented CCTP code entirely.

Commented-out code reduces readability and can be recovered from git history if needed. Since the CCTP flow is intentionally removed per the PR summary, delete these lines entirely.

Apply this diff:

-    // /**
-    //  * @notice Attests a CCTP payload and proves and executes it
-    //  * @param execParams_ The execution parameters
-    //  * @param cctpParams_ The CCTP parameters
-    //  * @param switchboardId_ The switchboard id
-    //  * @return success True if the payload was executed successfully
-    //  * @return returnData The return data from the execution
-    //  */
-    // function attestCCTPAndProveAndExecute(
-    //     CCTPExecutionParams calldata execParams_,
-    //     CCTPBatchParams calldata cctpParams_,
-    //     uint32 switchboardId_
-    // ) external payable returns (bool, bytes memory) {
-    //     address switchboard = socket__.switchboardAddresses(switchboardId_);
-    //     bytes32 payloadId = createPayloadId(
-    //         execParams_.executeParams.payloadPointer,
-    //         switchboardId_,
-    //         socket__.chainSlug()
-    //     );
-    //     ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions(
-    //         execParams_,
-    //         cctpParams_,
-    //         payloadId
-    //     );
-    //     (bool success, bytes memory returnData) = socket__.execute{value: msg.value}(
-    //         execParams_.executeParams,
-    //         TransmissionParams({
-    //             transmitterProof: execParams_.transmitterSignature,
-    //             socketFees: 0,
-    //             extraData: execParams_.executeParams.extraData,
-    //             refundAddress: execParams_.refundAddress
-    //         })
-    //     );
-
-    //     ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs);
-    //     return (success, returnData);
-    // }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 053fd3c and 3c8f98f.

📒 Files selected for processing (31)
  • FunctionSignatures.md (10 hunks)
  • PAYLOAD_ID_ARCHITECTURE.md (1 hunks)
  • contracts/evmx/interfaces/IConfigurations.sol (3 hunks)
  • contracts/evmx/interfaces/IWatcher.sol (1 hunks)
  • contracts/evmx/plugs/FeesPlug.sol (1 hunks)
  • contracts/evmx/watcher/Configurations.sol (10 hunks)
  • contracts/evmx/watcher/Watcher.sol (3 hunks)
  • contracts/protocol/Socket.sol (5 hunks)
  • contracts/protocol/SocketBatcher.sol (3 hunks)
  • contracts/protocol/SocketConfig.sol (6 hunks)
  • contracts/protocol/base/MessagePlugBase.sol (1 hunks)
  • contracts/protocol/base/PlugBase.sol (2 hunks)
  • contracts/protocol/interfaces/IPlug.sol (1 hunks)
  • contracts/protocol/interfaces/ISocket.sol (5 hunks)
  • contracts/protocol/interfaces/ISocketBatcher.sol (1 hunks)
  • contracts/protocol/interfaces/ISwitchboard.sol (2 hunks)
  • contracts/protocol/switchboard/FastSwitchboard.sol (3 hunks)
  • contracts/protocol/switchboard/MessageSwitchboard.sol (8 hunks)
  • contracts/protocol/switchboard/SwitchboardBase.sol (2 hunks)
  • contracts/utils/common/IdUtils.sol (1 hunks)
  • contracts/utils/common/Structs.sol (6 hunks)
  • deprecated/AuctionManager.sol (2 hunks)
  • deprecated/test/SetupTest.t.sol (1 hunks)
  • deprecated/test/evmx/Watcher.t.sol (6 hunks)
  • deprecated/test/mock/MockFastSwitchboard.sol (1 hunks)
  • deprecated/test/protocol/switchboards/FastSwitchboardTest.t.sol (2 hunks)
  • hardhat.config.ts (2 hunks)
  • test/SetupTest.t.sol (25 hunks)
  • test/SocketPayloadIdVerification.t.sol (1 hunks)
  • test/mocks/MockPlug.sol (1 hunks)
  • test/switchboard/MessageSwitchboard.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/protocol/base/PlugBase.sol
  • contracts/protocol/switchboard/SwitchboardBase.sol
🧰 Additional context used
🪛 Gitleaks (8.28.0)
deprecated/test/SetupTest.t.sol

[high] 47-47: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
PAYLOAD_ID_ARCHITECTURE.md

[grammar] ~24-~24: Ensure spelling is correct
Context: ...cher (on EVMX) - Verified by: Watcher offchain (links source) - Verification: `dst...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~36-~36: Ensure spelling is correct
Context: ...astSwitchboard - Verified by: Watcher offchain (verifies source) - Verification: `...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~120-~120: Ensure spelling is correct
Context: ...tches local config 2. Source (Watcher offchain): Verifies origin component matches e...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
PAYLOAD_ID_ARCHITECTURE.md

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

(MD040, fenced-code-language)

🔇 Additional comments (11)
hardhat.config.ts (2)

18-18: Import is necessary for HashZero usage below.

The import supports the change on line 40.


40-40: Confirm if read-only network configuration is intentional for Phase 1 or requires restoration.

The HashZero usage on line 40 is confirmed to be deliberate—all live networks (lines 54-79) receive HashZero accounts via getChainConfig(), while EVMX alone gets the actual privateKey (line 85). This pattern is intentional.

Technical facts:

  • All 21 networks configured as read-only (RPC connections only, no signing capability)
  • EVMX is the sole exception with 0x${privateKey}
  • The comment on lines 33-34 indicates privateKey setup is incomplete ("TODO: fix it for setup scripts")
  • Template literal wrapping ${constants.HashZero} is redundant since HashZero is already a string

The question stands: Is this security model (preventing transactions on all networks except local EVMX) the intended approach, or should private keys be configured for live networks once setup is finalized?

contracts/evmx/plugs/FeesPlug.sol (1)

113-119: LGTM: Type narrowing aligns with payload architecture migration.

The switchboardId_ parameter type change from uint64 to uint32 is consistent with the broader API surface migration described in the PR summary and matches changes across IPlug, ISocket, and related interfaces.

contracts/evmx/interfaces/IConfigurations.sol (1)

4-4: LGTM: Consistent uint64→uint32 migration for switchboard identifiers.

The type narrowing from uint64 to uint32 for switchboardId across getPlugConfigs, switchboards, setSwitchboard, and the simplified setIsValidPlug signature all align with the payload-based architecture migration.

Also applies to: 26-29, 36-42, 44-46

contracts/protocol/interfaces/IPlug.sol (1)

9-13: LGTM: IPlug.initSocket type migration is consistent.

The switchboardId_ parameter type change to uint32 aligns with the broader protocol-wide migration and matches changes in ISocket, SocketConfig, and related components.

contracts/protocol/interfaces/ISocketBatcher.sol (1)

21-28: LGTM: Consistent switchboardId type narrowing.

The uint32 type for switchboardId_ matches the interface updates across ISocket, IPlug, and SocketBatcher implementation.

deprecated/test/evmx/Watcher.t.sol (2)

384-384: LGTM: Bid.extraData type updated to bytes.

The change from string to bytes for extraData aligns with the API surface updates described in the PR summary and maintains consistency with the payload-based architecture.

Also applies to: 494-494


400-416: LGTM: Function calls updated to use updateRequest.

The transition from updateRequestAndProcessBatch to updateRequest is consistent with the API surface changes across the watcher/request handler flow.

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

13-21: LGTM: Consistent type migration and initialization flow update.

The switchboardId type change to uint32 and the updated initialization flow using socket__.connect(switchboardId_, "") align with the broader protocol migration toward payload-based architecture.

contracts/protocol/SocketBatcher.sol (1)

45-64: LGTM: Type migration for attestAndExecute.

The switchboardId_ parameter type change to uint32 is consistent with the broader protocol migration and matches ISocketBatcher interface updates.

contracts/evmx/watcher/Configurations.sol (1)

37-38: evmxSlug is properly initialized - no action required.

The Watcher.initialize() function accepts evmxSlug_ as a parameter (line 44) and assigns it to the inherited storage variable at line 50. The initialization path exists and works correctly through the contract's inheritance from Configurations. The concern in the original comment is unfounded.

Likely an incorrect or invalid review comment.

Comment on lines +4 to +11
import "solady/utils/Initializable.sol";
import "./Configurations.sol";
import {IPrecompile} from "../interfaces/IPrecompile.sol";
import {IFeesManager} from "../interfaces/IFeesManager.sol";
import {IPromise} from "../interfaces/IPromise.sol";
import {IERC20} from "../interfaces/IERC20.sol";
import "../../utils/common/IdUtils.sol";
import "solady/utils/LibCall.sol";
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

Missing imports for referenced symbols.

Lines 199-203 reference fromBytes32Format, AppGatewayAlreadyCalled, and InvalidCallerTriggered, but the necessary imports are not visible in this file. These symbols must be imported or inherited:

  • Add import {fromBytes32Format, toBytes32Format} from "../../utils/common/Converters.sol"; for the conversion utility
  • Ensure AppGatewayAlreadyCalled and InvalidCallerTriggered errors are imported or inherited (likely from a base contract or errors file)

If these are inherited from Configurations, the inheritance chain should make this explicit.

🤖 Prompt for AI Agents
In contracts/evmx/watcher/Watcher.sol (imports region lines ~4-11) and where
symbols are referenced around lines ~199-203, the file is missing imports for
fromBytes32Format/toBytes32Format and the custom errors AppGatewayAlreadyCalled
and InvalidCallerTriggered; add import {fromBytes32Format, toBytes32Format} from
"../../utils/common/Converters.sol"; and either import the errors from their
errors file or ensure the base contract that declares them (e.g.,
Configurations) is explicitly inherited by Watcher.sol so those error
identifiers are available.

Comment on lines +58 to 69
function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) external {
payloadData = rawPayload_;
currentPayloadId = getCurrentPayloadId(
payloadData.transaction.chainSlug,
payloadData.overrideParams.switchboardType
);
latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract(
appGateway_,
currentPayloadId
);
latestAppGateway = appGateway_;
}
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

Missing access control on addPayloadData.

This function stores critical payload data and deploys contracts but has no access control. Any external caller can:

  • Overwrite payloadData, currentPayloadId, latestAsyncPromise, and latestAppGateway
  • Deploy arbitrary async promise contracts
  • Potentially disrupt the execution flow

This should restrict access to authorized callers (e.g., onlyOwner, or a specific role/gateway).

🤖 Prompt for AI Agents
In contracts/evmx/watcher/Watcher.sol around lines 58 to 69, add access control
to addPayloadData which is currently externally callable and can overwrite
critical state and deploy contracts; restrict the function to an authorized
caller by applying an appropriate modifier (e.g., onlyOwner from OpenZeppelin
Ownable or a specific role check via AccessControl) or by validating that
msg.sender == an allowed gateway address, and update constructor/imports to set
the owner/role or authorized gateway(s); keep the same logic inside the function
but add the require/onlyOwner/hasRole check so only authorized accounts can call
addPayloadData.

}

function testRequestHandlerUpdateRequestAndProcessBatch() public {
function testRequestHandlerupdateRequest() public {
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

Fix test function name typo.

The function name testRequestHandlerupdateRequest is missing camelCase capitalization. This should be testRequestHandlerUpdateRequest.

Apply this diff:

-    function testRequestHandlerupdateRequest() public {
+    function testRequestHandlerUpdateRequest() public {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function testRequestHandlerupdateRequest() public {
function testRequestHandlerUpdateRequest() public {
🤖 Prompt for AI Agents
In deprecated/test/evmx/Watcher.t.sol around line 400 rename the test function
from testRequestHandlerupdateRequest to testRequestHandlerUpdateRequest; update
the function declaration and any references/calls to it within the file (and any
test runner if referenced elsewhere) to use the corrected camelCase name so the
test compiles and follows naming conventions.

Comment on lines +528 to +548
function predictAsyncPromiseAddress(
address invoker_,
address forwarder_
) internal returns (address) {
bytes memory asyncPromiseBytecode = type(AsyncPromise).creationCode;
bytes memory constructorArgs = abi.encode(invoker_, forwarder_, address(addressResolver));
bytes memory combinedBytecode = abi.encodePacked(asyncPromiseBytecode, constructorArgs);

bytes32 salt = keccak256(abi.encodePacked(constructorArgs, asyncPromiseCounter++));

bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(addressResolver),
salt,
keccak256(combinedBytecode)
)
);

return address(uint160(uint256(hash)));
}
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

Import AsyncPromise so this file compiles.

This should reference AsyncPromise, but the contract is never imported, so type(AsyncPromise).creationCode is an unknown identifier and the build breaks. Add the proper AsyncPromise import before using it.

🤖 Prompt for AI Agents
In deprecated/test/SetupTest.t.sol around lines 528-548 the code uses
type(AsyncPromise).creationCode but AsyncPromise is never imported, causing a
compile error; add the correct import for the AsyncPromise contract at the top
of the file (adjust the relative path to where AsyncPromise.sol is located in
the repo) so the identifier is defined and the file compiles, then run a quick
compile to verify the path is correct.

Comment on lines +34 to +36
// if (!process.env.SOCKET_PRIVATE_KEY) throw new Error("No private key found");
const privateKey: HardhatNetworkAccountUserConfig = process.env
.SOCKET_SIGNER_KEY as unknown as HardhatNetworkAccountUserConfig;
.SOCKET_PRIVATE_KEY as unknown as HardhatNetworkAccountUserConfig;
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

🧩 Analysis chain

EVMX will break silently if SOCKET_PRIVATE_KEY is not set.

The guard on line 34 is commented out, so there's no validation that SOCKET_PRIVATE_KEY exists. Since line 85 still uses privateKey for the EVMX network, missing this environment variable will result in accounts: ['0xundefined'], causing deployment failures.

Either uncomment the guard or add a specific check before the EVMX configuration.


🏁 Script executed:

#!/bin/bash
# Verify if EVMX network configuration depends on privateKey
rg -n -A3 -B3 'EVMX.*accounts'

Length of output: 0



🏁 Script executed:

#!/bin/bash
# Search for EVMX network configuration (broader search)
rg -n 'EVMX' hardhat.config.ts

# Extract the context around lines 34-36 and line 85
echo "=== Lines 30-40 (privateKey assignment area) ==="
sed -n '30,40p' hardhat.config.ts

echo "=== Lines 80-95 (network config area) ==="
sed -n '80,95p' hardhat.config.ts

# Search for how privateKey is used
echo "=== All uses of privateKey ==="
rg -n 'privateKey' hardhat.config.ts

Length of output: 1352


🏁 Script executed:

#!/bin/bash
# Search for EVMX network configuration (broader search)
rg -n 'EVMX' hardhat.config.ts

# Extract the context around lines 34-36 and line 85
echo "=== Lines 30-40 (privateKey assignment area) ==="
sed -n '30,40p' hardhat.config.ts

echo "=== Lines 80-95 (network config area) ==="
sed -n '80,95p' hardhat.config.ts

# Search for how privateKey is used
echo "=== All uses of privateKey ==="
rg -n 'privateKey' hardhat.config.ts

Length of output: 1352


EVMX will fail silently if SOCKET_PRIVATE_KEY is not set.

The guard on line 34 is commented out, leaving no validation for the SOCKET_PRIVATE_KEY environment variable. When this variable is missing, privateKey becomes undefined. Line 85 then constructs accounts: ['0xundefined'] for the EVMX network, which will cause deployment to fail with an invalid account configuration.

Other networks use getChainConfig() which safely defaults to HashZero, but EVMX bypasses this and fails hard on misconfiguration without a clear error message.

Uncomment the guard or add a specific check before the EVMX configuration block.

🤖 Prompt for AI Agents
In hardhat.config.ts around lines 34 to 36, the guard validating
SOCKET_PRIVATE_KEY is commented out causing privateKey to be undefined and
producing invalid accounts for the EVMX network; reintroduce a check that throws
a clear error if process.env.SOCKET_PRIVATE_KEY is missing (or set a safe
default like HashZero) before the EVMX configuration, and use the validated
value when building the accounts array so EVMX never receives '0xundefined'.

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: 0

♻️ Duplicate comments (3)
contracts/evmx/fees/Credit.sol (3)

16-75: Fix missing OverrideParamsLib import.

OverrideParamsLib is not in scope here, so compilation fails. This should explicitly import the library before the using directive.

+import "../../utils/OverrideParamsLib.sol";

259-277: Remove invalid async keyword and build override params locally.

The body uses the JavaScript async keyword and writes to an undeclared overrideParams, so the contract will not compile. This should drop async, construct a fresh OverrideParams in memory via the library, and pass that into the RawPayload before calling the watcher.

-    ) internal async {
-        overrideParams = overrideParams.setMaxFees(getMaxFees(chainSlug_)).setConsumeFrom(
-            consumeFrom_
-        );
-
-        RawPayload memory rawPayload;
-        rawPayload.overrideParams = overrideParams;
+    ) internal {
+        OverrideParams memory params;
+        params = params.setMaxFees(getMaxFees(chainSlug_)).setConsumeFrom(consumeFrom_);
+
+        RawPayload memory rawPayload;
+        rawPayload.overrideParams = params;
         rawPayload.transaction = Transaction({
             chainSlug: chainSlug_,
             target: _getFeesPlugAddress(chainSlug_),
             payload: payload_
         });
-        watcher__().addPayloadData(rawPayload, address(this));
+        watcher__().addPayloadData(rawPayload, address(this));
     }

221-223: Don't clobber allowances when the watcher pulls credits.

Forcing _approve here overwrites whatever allowance the user previously set (including infinite approvals). This should short-circuit to an internal transfer whenever the watcher is the caller so allowances stay untouched.

-        if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
-        return super.transferFrom(from_, to_, amount_);
+        if (msg.sender == address(watcher__())) {
+            _transfer(from_, to_, amount_);
+            return true;
+        }
+        return super.transferFrom(from_, to_, amount_);
🧹 Nitpick comments (1)
test/SetupTest.t.sol (1)

161-186: Move hoax closer to the external call for clarity.

The hoax(watcherEOA); at line 176 works correctly (it sets msg.sender for the next external call at line 185), but placing it immediately before watcher.setAppGatewayConfigs(params); would improve readability and make the authorization clearer.

Apply this diff:

     WatcherMultiCallParams memory params = WatcherMultiCallParams({
         contractAddress: address(watcher),
         data: abi.encode(configs),
         nonce: watcherNonce,
         signature: _createWatcherSignature(address(watcher), abi.encode(configs))
     });
     watcherNonce++;
-    hoax(watcherEOA);
-
-    WatcherMultiCallParams memory params = WatcherMultiCallParams({
-        contractAddress: address(watcher),
-        data: abi.encode(configs),
-        nonce: watcherNonce,
-        signature: _createWatcherSignature(address(watcher), abi.encode(configs))
-    });
-    watcherNonce++;
+    hoax(watcherEOA);
     watcher.setAppGatewayConfigs(params);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8f98f and 1334556.

📒 Files selected for processing (5)
  • contracts/evmx/fees/Credit.sol (7 hunks)
  • contracts/evmx/interfaces/IFeesManager.sol (2 hunks)
  • contracts/evmx/interfaces/IFeesPlug.sol (1 hunks)
  • contracts/evmx/plugs/FeesPlug.sol (2 hunks)
  • test/SetupTest.t.sol (26 hunks)
🔇 Additional comments (12)
test/SetupTest.t.sol (12)

10-10: LGTM: Utils integration added.

The Utils import and inheritance provide helper functions for the test setup.

Also applies to: 35-35


64-64: LGTM: Fee amount is appropriate for tests.

0.01 ether is a reasonable test value.


109-109: LGTM: Large initial balance appropriate for tests.

100000 ether ensures sufficient funds for all test scenarios.


126-128: LGTM: Precompile registration properly gated.

The setPrecompile calls are correctly executed within the watcherEOA prank context.


428-441: LGTM: Deposit flow with proper watcher impersonation.

The hoax correctly gates the feesManager.deposit call, and assertions validate the expected balance changes.


505-541: LGTM: Payload execution flow is well-structured.

The branching logic for READ/WRITE/SCHEDULE types is clear, and the time manipulation for SCHEDULE (warp to deadline - expiryTime) and failure cases (warp to deadline) correctly simulates the execution timeline.


584-602: LGTM: Proof upload properly gated.

The hoax correctly impersonates watcherEOA before calling writePrecompile.uploadProof.


722-734: Verify watcher authorization mechanism for markRevert.

This has the same concern as _resolvePayload: past reviews indicated watcher.markRevert is onlyWatcher-gated, but there's no impersonation before the call. The signature-based WatcherMultiCallParams suggests the authorization mechanism may have changed. The script in the previous comment will verify this for both methods.


748-810: LGTM: Comprehensive payload verification.

The field-by-field validation of Payload structs is thorough and aligns with the payload-first architecture migration.


836-847: LGTM: Payload ID calculation aligns with new architecture.

The createPayloadId helper properly encodes origin and verification chain/switchboard information. The inline comments clearly document the structure.


899-901: LGTM: DigestParams align with payload-first architecture.

The changes properly reflect the migration: source now encodes chain and plug information, prevBatchDigestHash no longer uses legacy triggerId, and extraData management is delegated to the contract. Comments clearly document the rationale.


711-720: The original review comment is based on outdated implementation details.

The authorization mechanism has shifted from onlyWatcher modifier (which would require hoax) to signature-based validation via _validateSignature(). Watcher.sol's resolvePayload() now accepts WatcherMultiCallParams with a signature field, which the test properly creates using _createWatcherSignature(). No hoax is needed or appropriate for this signature-based authorization pattern. The test implementation is correct.

Likely an incorrect or invalid review comment.

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.

4 participants