-
Notifications
You must be signed in to change notification settings - Fork 9
Phase 1 #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Phase 1 #204
Conversation
WalkthroughMigrates 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
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
%% 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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes This should be reviewed thoroughly. Pay extra attention to:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this 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 errorDeadlinePassed() 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 toupdateRequest(requestCount)indeprecated/PromiseResolver.solmust include thefeesUsedargument 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 failsOn withdraw failure you mint
creditAmount_again, then setnativeAmount_ = 0before 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.resolvePayloadnow expectsPayload calldata, but this override still takesPayloadParams 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
_initializeWatcherand_initializeOwnerduring deployment. After droppingWatcherBase/Initializablethere is no path that sets the owner or the address resolver, so everyonlyOwner/onlyWatcherentry 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, andParallel, but those enums were removed fromStructs.sol. This should drop the imports, otherwise compilation fails. Lines 67-88 manipulateoverrideParams.isAsyncModifierSet, yetOverrideParamsno 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 setsisValidPromise[promise_] = trueeven thoughpromise_is undefined after the previous retrieval was commented out. This should reinstate the watcher call (or otherwise provide the promise address) before assigning, or_postAsyncwill not compile.
🧹 Nitpick comments (13)
deprecated/RequestHandler.sol (1)
92-98: getPrecompileFees should validate callTypeWithout 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 importsThe 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 importscontracts/evmx/interfaces/IAuctionManager.sol (1)
4-4: Remove unused Structs importsThis 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 importsFunctionSignatures.md (1)
482-482: Rename noted; add deprecation note and cross-referenceupdateRequest 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 clarityDeclaring
RawPayload memory RawPayload;shadows the type and hurts readability. This should berawPayload.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 RawPayloadshadows 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 intentConsider renaming to
testRequestHandlerUpdateRequestfor 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 APIComment 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 handlePayloadhandlePayload 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 datacontracts/evmx/helpers/Forwarder.sol (1)
93-106: RenameRawPayloadvariable in Forwarder.sol torawPayload- 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 settingZero 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 polarityincreaseFees 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
📒 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 reasonableclear() initializes sensible defaults and threads switchboardType. Setters correctly mutate memory and return self.
| /// @param payload The payload parameters to resolve | ||
| function resolvePayload(Payload calldata payload) external; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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_withPayload calldata payload - SchedulePrecompile.sol:140 — replace
PayloadParams calldata payloadParams_withPayload 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.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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); | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| /// @notice Clears the call parameters array | ||
| function clearQueue() public { | ||
| delete payloadQueue; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /// @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.
| 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(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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
ReplacepayloadIdwith auint256 feesUsedvalue inupdateRequest(requestCount, …)at Watcher.t.sol:409 and 415.contracts/evmx/fees/Credit.sol (1)
139-151: Critical: double-mint bug when native transfer failsIf
feesPool.withdrawfails, the code mintscreditAmount_again and then adds 0 tocreditAmount_. 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 bytesThis should switch to
ForwarderParams calldataandbytes calldatato 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:overrideParamsis undefined.Line 140 accesses
overrideParams.consumeFrom, butFeesManagerdoes not declare anoverrideParamsstate 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.gasLimitis non-zero, the function never assigns it to the return valuegasLimit, so the function returns0. This incorrect value flows intoprecompileData(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 fromConverters.solAppGatewayAlreadyCalledandInvalidCallerTriggerederrors (used at lines 173, 177) need import fromErrors.solimport "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 herecontracts/protocol/SocketBatcher.sol (1)
66-103: Remove dead/commented CCTP code or guard it behind a feature flagLarge 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
📒 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_isWatcherfunction has always comparedaccount_directly towatcher__(); 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.solfiles returned no matches fordeployForwarder__. 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
switchboardTypefield, but the struct definition is correct—switchboardTypeis accessible viarawPayload.overrideParams.switchboardType(line 129). The issue is in consuming code (e.g.,WritePrecompile.solline 117,ForwarderV2.solline 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
overrideParamspublic state variable (line 27) provides a single source of truth for override configuration, avoiding scattered state. ThegetOverrideParams()(lines 201-203) andapplyOverride()(lines 213-215) methods complete the fluent builder pattern enabled byOverrideParamsLib.contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)
115-118: Correct nested access toswitchboardType.Line 117 correctly accesses
rawPayload.overrideParams.switchboardTypeinstead of the non-existentrawPayload.switchboardType. This aligns with theRawPayloadstructure defined inStructs.sol(lines 143-147) whereswitchboardTypeis a field of the nestedOverrideParams.contracts/evmx/watcher/precompiles/ReadPrecompile.sol (1)
44-69: Clean RawPayload handling with proper access control.The
handlePayloadfunction correctly usesRawPayload(line 45) and accesses nested fields likerawPayload.overrideParams.readAtBlockNumber(line 59) andrawPayload.transaction(lines 58, 65). TheonlyWatchermodifier (line 50) properly gates access. This implementation aligns with the payload-centric model and mirrors the pattern inWritePrecompile.sol.contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2)
28-33: LGTM!The explicit
watcher__state variable andonlyWatchermodifier provide clear access control. This is more transparent than inheriting fromWatcherBase.
117-145: Remove manual overflow guard: Solidity 0.8.21 uses checked arithmetic soblock.timestamp + rawPayload.overrideParams.delayInSecondsreverts on overflow. Pattern is consistent across precompiles.contracts/evmx/interfaces/IWatcher.sol (1)
40-49: LGTM!The
executePayloadmethod signature correctly acceptsRawPayloadandappGateway_, 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, matchingbytes32 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
Theif (nextPayloadCount == 0) nextPayloadCount = 1guard ensures pointers start at 1, andpayloadPointer: nextPayloadCount++aligns with the emittedcurrentPayloadId.
70-109: Approve executePayload ordering
currentPayloadIdis assigned at lines 79–82 before callingblockCreditson 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.
contracts/evmx/fees/Credit.sol
Outdated
| // 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)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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)); | |
| } |
contracts/evmx/fees/FeesManager.sol
Outdated
| // address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom; | ||
| address consumeFrom = overrideParams.consumeFrom; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Passed as a function parameter to
unblockAndAssignCredits, or - Retrieved from payload storage (e.g.,
watcher__.getPayload(payloadId_).consumeFrom), or - 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.
| // 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.
| /// @notice The payload count of the promise | ||
| bytes32 public override payloadId; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /// @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; |
| /// @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.
| 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; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
requestCountinblockCredits/unblockAndAssignCreditscalls with the correctbytes32 payloadId(e.g.r.payloadId). - Add or update NatSpec on
IFeesManager.solforpayloadId_to clarify its purpose. - Audit any storage mappings or events previously keyed by
uint40 requestCountto usebytes32 payloadIdinstead.
🤖 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.
| /// @notice The payload id of the promise | ||
| function payloadId() external view returns (bytes32); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 2Length 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 requestCountparameters and returns withbytes32 payloadIdin active contracts (e.g.contracts/evmx/helpers/AsyncDeployer.sol,contracts/evmx/interfaces/IAsyncDeployer.sol,contracts/evmx/base/AppGatewayBase.sol) - Remove all residual
requestCountreferences outsidedeprecated/tests - Update events, variables, and calls to use
payloadId()instead ofrequestCount()
🤖 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 newbytes32 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.
| /// @notice Error thrown when payload count mismatch | ||
| error PayloadCountMismatch(); | ||
|
|
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| RawPayload[] memory RawPayload = new RawPayload[](0); | ||
|
|
||
| hoax(watcherAddress); | ||
| requestHandler.submitRequest( | ||
| 1000, | ||
| address(0x1), | ||
| address(0x2), | ||
| address(0x3), | ||
| queueParams, | ||
| RawPayload, | ||
| "" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
submitRequestAPI expects a singleRawPayload calldata, not an array. ReplacewithRawPayload[] memory RawPayload = new RawPayload[](0);
RawPayload memory rawPayload = /* construct valid RawPayload struct */;
- Rename the variable to
rawPayloadand 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.
There was a problem hiding this 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
uint40withbytes32at line 27 forcespayloadIdinto a new slot, shiftinglocalInvoker(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:
- 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;
- Add
payloadIdafter 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 settingpayloadIdwithout 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 comparingpayloadId(bytes32) not counts. Consider renaming toPayloadIdMismatchin a follow-up if it won't break error handling.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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) exposesswitchboardsas a public mapping with signaturemapping(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
onlyWatchermodifier. Since this sets a configuration parameter, verify this is the intended access control rather thanonlyOwner. 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 oldrequestCount_(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, andOverrideParamsLibusage throughout the contract.
99-109: LGTM: Transaction control functions updated for payload model.Both
_revertTxand_increaseFeescorrectly usebytes32 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
overrideParamsfor 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
OverrideParamsLibis clearer and more composable than the previous approach. Theclear()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
getCurrentPayloadIdtype mismatch flagged separately). The API surface is clear and complete for the payload-centric execution model.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| function _settlePayload(bytes32 payloadId_, uint256 feesUsed_) internal { | ||
| feesManager__().unblockAndAssignCredits(payloadId_, address(feesManager__())); | ||
| emit PayloadSettled(payloadId_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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 failureOn 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 amountisCreditSpendable 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 precompileDatalast 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 meansgetAsyncPromiseAddressneeds 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:
getAsyncPromiseAddressis called to predict an addressdeployAsyncPromiseContractis called, incrementing the counter- The predicted address won't match the deployed address if other deployments happen in between
Consider either:
- Passing the counter value to
getAsyncPromiseAddress- Removing the counter from the salt and using only
payloadId_+invoker_for uniqueness (if guaranteed unique)- Returning the deployed address from
deployAsyncPromiseContractand not relying on predictionRun this script to check how
getAsyncPromiseAddressis 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=solcontracts/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, keepingonlyWatcheris consistent; fix the doc.- /// @dev Only callable by the contract owner + /// @dev Only callable by the watchercontracts/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 executePayloadoverrideParams 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: UndefinedoverrideParamsusage causes compilation failure and wrong data source for consumeFrom.FeesManager doesn’t declare
overrideParamsand likely doesn’t inherit it. These references fail compilation and incorrectly fetchconsumeFrom. This should read from the watcher payload for the givenpayloadId_. Also remove the strayoverrideParamswrites in initialize/setMaxFees (they don’t belong here and are redundant withmaxFeesPerChainSlug).Apply these diffs:
- 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;
- 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.solintentionally exposesoverrideParams, 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.
_validateonly assigns defaults when override is zero, but never initializesgasLimitfrom 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 usesbytes32 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), andInvalidCallerTriggered(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):
AppGatewayAlreadyCalledInvalidCallerTriggeredInsufficientFeesInvalidCallTypeDeadlinePassedDeadlineNotPassedForOnChainRevertOnlyAppGatewayNewMaxFeesLowerThanCurrentInvalidContractInvalidDataInvalidSignatureNonceUsed
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
unblockAndAssignCreditsaccepts a thirdfeesUsed_parameter. If not, update the IFeesManager interface accordingly.
🧹 Nitpick comments (7)
test/SetupTest.t.sol (1)
721-722: Watcher-only call should impersonate watchermarkRevert 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 callsitesWith 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 sincecounteris 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 unnamedaddressparameter (line 119). While syntactically valid in Solidity, this violates consistency with the interface contract. WritePrecompile correctly names itappGateway, but ReadPrecompile has the same issue. This should be renamed toappGatewayto 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 idcontracts/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
onlyOwnerto 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
📒 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 uint40requestBlockedCredits 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 goodsetupGatewayAndPlugs composes AppGatewayConfig correctly and applies via watcher.setAppGatewayConfigs.
160-177: App gateway config composition LGTMConfig array sizing and field population are tight; hoax(watcherEOA) before set is correct.
228-230: Consistent watcher wiringsetSocket and setSwitchboard with FAST constant align with the new config model.
712-715: Resolve flow fee parameterization LGTMresolvePayload 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__() callThe broadened access in
isWatcher()allowingwatcher__().owner()conflicts with the modifier's NatSpec ("watcher precompile") and diverges from most other implementations (AddressResolver,ReadPrecompile,SchedulePrecompileall enforce strict precompile-only checks). OnlyWritePrecompileintentionally 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 thewatcher__()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
asyncPromiseCounterincrement 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:
- Creates the Counter instance
- Initializes the socket connection
- Sets up gateway and plug infrastructure
- 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.solpassescurrentPayloadId(bytes32) todeployAsyncPromiseContractAsyncDeployer.solimplementation correctly encodes bytes32 in constructor args and init data- Salt computation via
keccak256(abi.encodePacked(constructorArgs, asyncPromiseCounter))handles bytes32 without issueDeprecated tests still use uint40, but that's immaterial. Implementation is sound.
contracts/utils/OverrideParamsLib.sol (2)
12-26: LGTM: sane defaults and fluent builder.
clearsets WRITE, LOW finality, zero fees, and acceptsswitchboardType. 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 minimalRawPayloadwith overrides is consistent with the new flow.contracts/evmx/watcher/Configurations.sol (1)
114-121: setIsValidPlug: scope is appropriate.Binding to
msg.senderensures 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
maxFeesbefore computing actual fees from the precompile (line 95-96). If the actualfeesreturned by the precompile is less thanmaxFees, 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.
createPayloadIdis 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 sincegetCurrentPayloadIdreceiveschainSlug_as input and uses it to look up the switchboard configuration. UsingevmxSlugwould 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| isAsyncModifierSet = true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| 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_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| function withdrawCredits( | ||
| uint32 chainSlug_, | ||
| address token_, | ||
| uint256 amount_, | ||
| address receiver_ | ||
| ) external { | ||
| _withdrawCredits(chainSlug_, token_, amount_, receiver_); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| function increase() external; | ||
|
|
||
| function getCounter() external; | ||
|
|
||
| // A function that is not part of the interface, used for testing on-chian revert. | ||
| function wrongFunction() external; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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 failson 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 balanceOfIf 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 forsocket.You compute
socketas 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
socketas 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 forfeesPlug.Same issue:
feesPlugis 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 throughConstructor 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 counterpartBoth 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 2abi.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 precompileDataWritePrecompile 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: Invalidoverridemodifier on state variable.State variables cannot use the
overridekeyword in Solidity. This will cause a compilation error. Remove theoverridemodifier.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:
isAsyncModifierSetis set totruein_preAsync(line 67) but never reset tofalsein_postAsync. This leaves the async guard permanently open after the first async call, allowing functions like_setScheduleto be called outside their intended async scope.No validation that
promise_is non-zero before marking it valid (line 73). IfexecutePayload()returnsaddress(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 libraryusing 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 watcherApproving 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.solThe function
_callAppGatewaysusesfromBytes32Formatand 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 usesbytes32. 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 salthardhat-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 functionsUpdate the title/notice/dev comments to match the implementation.
80-112: Access-control docs mismatchNatSpec 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 SchedulePrecompileSchedulePrecompile 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 precompilesRead/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 onlyWatcherFix NatSpec to reflect onlyWatcher authorization.
contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)
68-73: Access control inconsistency with other precompilesWritePrecompile 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 sizeEmitting 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)andcontractAddress_(line 176), but when called at line 79,contractAddress_is alsoaddress(this). This meansaddress(this)appears twice in the digest. Is this intentional? IfcontractAddress_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
📒 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
callTypeis now correctly set toSCHEDULE(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
_revertTxand_increaseFeesfunctions correctly usepayloadId_with the new watcher APIs (cancelExecutionandincreaseFees).
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.maxFeesfor 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
OverrideParamsstate usingOverrideParamsLib.
109-136: LGTM! Utility functions are correct.The
getOnChainAddress,_createContractId, and_getCurrentPayloadIdfunctions correctly implement address resolution and payload ID retrieval in the new payload-based model.
222-222: LGTM! Virtual hook is correct.The
handleReverthook 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 ifasyncPromiseCounterdoesn'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:
getAsyncPromiseAddressreads the currentasyncPromiseCounterto predict an address, but the counter can increment between prediction and deployment, making the predicted address stale.However, this function is exposed in the
IAsyncDeployerinterface but not called in production code—deployAsyncPromiseContractinWatcher.soldeploys 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
getAsyncPromiseAddressfor off-chain address prediction, this race condition is a bug that needs fixing (salt should use onlypayloadId_if it's guaranteed unique per deployment, or remove the function entirely).- If
getAsyncPromiseAddressis 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 removedGood cleanup aligned with plugs list. No further action.
contracts/evmx/fees/Credit.sol (1)
36-39: Storage key change for blocked credits requires migration planSwitching 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-exportedChainSlug is defined in
src/chain-enums/chainSlug.tsand re-exported throughsrc/index.tsvia theexport * 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 codebaseThe 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 ContractFactoryPlughardhat-scripts/deploy/3.configureChains.ts:150— Reads ContractFactoryPlug from chainAddresseshardhat-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.tsThis suggests either:
- Incomplete refactoring — Must clean up all ContractFactoryPlug references system-wide, or
- 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 variableoverrideParamsis 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, orconfigurationsEvents, 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_frommsg.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 solidChecks target/payload, computes deadline, and emits with payloadId. No issues.
contracts/evmx/watcher/Watcher.sol (1)
256-259: Suggested fix is incompatible with function signatureThe
unblockAndAssignCreditsmethod accepts only 2 parameters (bytes32 payloadId_,address assignTo_), not 3. PassingfeesUsed_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 inFeesManager.solandIFeesManager.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.senderinstead of an explicitappGateway_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).
| ) 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({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /// @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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /// @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).
| function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -B2Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for evmxSlug initialization without type restriction
rg -n 'evmxSlug' -A3 -B3 contracts/evmx/watcher/Configurations.solLength of output: 518
🏁 Script executed:
#!/bin/bash
# Look for constructor in Configurations contract
rg -n 'constructor\s*\(' contracts/evmx/watcher/Configurations.solLength of output: 0
🏁 Script executed:
#!/bin/bash
# Search for initialize function
rg -n 'function\s+initialize' contracts/evmx/watcher/Configurations.solLength of output: 0
🏁 Script executed:
#!/bin/bash
# Get full context of the contract to understand initialization strategy
head -100 contracts/evmx/watcher/Configurations.solLength of output: 4223
🏁 Script executed:
#!/bin/bash
# Search for any setter or assignment to evmxSlug
rg -n 'evmxSlug\s*=' contracts/evmx/watcher/Configurations.solLength 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 -20Length 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 -150Length 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.
| 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_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
hardhat-scripts/deploy/2.roles.ts
Outdated
| export const REQUIRED_ROLES = { | ||
| EVMx: { | ||
| AuctionManager: [ROLES.TRANSMITTER_ROLE], | ||
| // AuctionManager: [ROLES.TRANSMITTER_ROLE], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.FastSwitchboardAlso 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 || trueLength 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 -80Length 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.FastSwitchboardCommittable 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.
| // await updateContractSettings( | ||
| // EVMX_CHAIN_ID, | ||
| // Contracts.AddressResolver, | ||
| // "defaultAuctionManager", | ||
| // [], | ||
| // evmxAddresses[Contracts.AuctionManager], | ||
| // "setDefaultAuctionManager", | ||
| // [evmxAddresses[Contracts.AuctionManager]], | ||
| // signer | ||
| // ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.tsLength 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 -B1Length 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 -C2Length 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 anywheresetDeployForwarder: no active calls exist anywheresetWatcherCoreContracts: 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.
There was a problem hiding this 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
CounterSetFeesbut 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 acceptsgrantRolefrom 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 thetrueflag or callgetSocketSignerdirectly) 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, andTRIGGER_FEES. Verify this matches theWatcher.initializesignature.#!/bin/bash # Verify Watcher.initialize signature matches the deployment parameters ast-grep --pattern $'function initialize($$$) { $$$ }' contracts/evmx/watcher/Watcher.solcontracts/utils/common/Structs.sol (1)
143-147: Verify consumers access switchboardType via overrideParams.The past review comment mentioned that
RawPayloadis missingswitchboardType. The field is now nested inoverrideParams.switchboardType. Ensure all consumers (e.g., WritePrecompile, ForwarderV2) access it via the nested pathrawPayload.overrideParams.switchboardType.#!/bin/bash # Search for rawPayload.switchboardType usage to verify correct access pattern rg -nP 'rawPayload\.switchboardType' --type=solcontracts/evmx/watcher/Watcher.sol (3)
86-95: Block actual fees after computing them, not maxFees upfront.Lines 86-90 block
maxFeesbefore 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 tounblockAndAssignCredits. The FeesManager needs this value to properly refund unused credits vs. charge actual fees used.Verify the
unblockAndAssignCreditssignature 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.
cancelExecutionsettles withr.maxFeesat 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.
RawPayloadis 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
gasLimitinside theifblock whenoverrideParams.gasLimit == 0. When a caller provides a non-zero override,gasLimitremains 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 usedigestterminology.The comment refers to
payloadIdbut the mapping is now exclusively accessed usingdigestvariables 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
feesAmountis 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
countervariable 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
uploadPlugcall 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
ContractFactoryPlugis 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
📒 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_todigestcorrectly reflects the architectural shift described in the PR. This aligns with the emit statement on line 53 and thedigest_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.solresolves 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/totest/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 howEVMxAddressesObjis 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 bothERC1967FactoryandFeesPool, 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
feesAmountconstant is properly used in_resolvePayloadfor 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.setPrecompileis straightforward and properly authorized within the watcher EOA prank block.
147-185: LGTM: Gateway setup refactored correctly.The
setupGatewayAndPlugsconsolidates gateway and plug wiring into a single path. Authorization is properly handled viahoax(watcherEOA)and signature verification.
439-452: LGTM: Deposit flow simplified.Direct interaction with
feesManageris cleaner than the previous approach. Balance assertions correctly verify credit and native deposits.
610-612: LGTM: Authorization handled correctly.Using
hoax(watcherEOA)beforewritePrecompile.uploadProofproperly grants watcher authority for the call.
758-820: LGTM: Payload validation complete.The
checkPayloadfunction is fully migrated to usePayload[]with comprehensive field assertions. All new fields (consumeFrom,maxFees) are properly validated.
721-744: No issues found. Authorization is properly implemented.Both
watcher.resolvePayloadandwatcher.markRevertvalidate signatures via_validateSignaturebefore executing any logic. The signature-based authorization usingWatcherMultiCallParamsis the intended and correct mechanism—not a deviation from standard patterns. The nonce prevents replay attacks. Tests using_createWatcherSignatureare following the proper authorization flow.
524-552: This review comment is incorrect.The concern about underflow is unfounded. Across all precompiles (ReadPrecompile, WritePrecompile, SchedulePrecompile),
deadlineis consistently set by addingexpiryTimeto a current/future timestamp:deadline = block.timestamp + expiryTime(orexecuteAfter + expiryTime). This guaranteesdeadline >= block.timestamp + expiryTime, makingdeadline - expiryTime >= block.timestampalways true. The test's time warping todeadline - expiryTimeanddeadlineis 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
prevBatchDigestHashdirectly in the keccak256 encoding without validation or special handling. Usingbytes32(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.sampleline 24 already containsSOCKET_PRIVATE_KEYwith the correct variable name, matching the code change inhardhat.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.incrementCountersintest/apps/counter/CounterAppGateway.sol:29expects a singleaddressparameter, 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, andauctionManagerEventsare 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 withif (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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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); |
There was a problem hiding this comment.
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.
| 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.
| | `CreditsBlocked` | `(payloadId: bytes32, consumeFrom: address, amount: uint256)` | `0xe0ce1c6e339ba1b699d262b081adbc74ddc8699c19405e3a8459940944ccd9ea` | | ||
| | `CreditsUnblocked` | `(payloadId: bytes32, consumeFrom: address)` | `0xe19214f41bd8f45a4fa569e176cdb3700de18b99f163f385cdfd210118dc7aa3` | | ||
| | `CreditsUnblockedAndAssigned` | `(payloadId: bytes32, consumeFrom: address, transmitter: address, amount: uint256)` | `0xf2fa1621e1a549c353279ffa16145321c7297cd56fd8fe4fa0d6d4b9ea09518c` | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 be0xade089e13c1bf6bdd5444d99c6ba16a141ddeaaa409cff5f373edf351031780fCreditsUnblocked: should be0xcd0d8d53b493fdf455aa5e7c5e8b6520a1b50baf8c03f39ab7cdd6f2fd4494d7CreditsUnblockedAndAssigned: should be0x24c2fc23779c30f40dae52a3ddcec2a5107ead70d6b3767c7ea5e6e98cd384b8
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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=tsLength of output: 3582
🏁 Script executed:
#!/bin/bash
# Find signWatcherMessage implementation and params consumption
rg -nP 'signWatcherMessage|params\s*,' --type=ts -C3 | head -100Length 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=tsLength 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 -C3Length of output: 1005
🏁 Script executed:
#!/bin/bash
# View the full signWatcherMessage function implementation
cat hardhat-scripts/utils/sign.tsLength of output: 2240
🏁 Script executed:
#!/bin/bash
# Search for setAppGatewayConfigs to see how it uses the params
rg -nP 'setAppGatewayConfigs' --type=ts -A5 -B5Length 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 -50Length of output: 951
🏁 Script executed:
#!/bin/bash
# Get the full signWatcherMultiCallMessage implementation to see return type
cat src/signer.tsLength 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.tsline 62 converts to number:nonce: Number(nonce)hardhat-scripts/deploy/6.connect.tsline 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.
| const params: WatcherMultiCallParams = { | ||
| contractAddress: targetContractAddress, | ||
| data: calldata, | ||
| nonce, | ||
| nonce: Number(nonce), | ||
| signature, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| Counter counter = new Counter(); | ||
| counter.initSocket(toBytes32Format(appGateway), socket, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
…tocol into feat/message-switchboard
feat: message switchboard
There was a problem hiding this 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, andOverrideParamsLibare 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_createRequestimplementationThis block will not compile: Solidity has no
asynckeyword,overrideParamsis never declared, and the user-suppliedmaxFees_argument is ignored (everything is forced togetMaxFees(chainSlug_)). Declare a localOverrideParams, populate it (while honoring the caller’smaxFees_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 tosetMaxFees. This should restore both compilation and the intended fee behaviour.
♻️ Duplicate comments (3)
contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)
8-11: ImportRawPayloadso this file compiles.
RawPayloadis used in the event signature and function arguments, but the Structs import only pulls inDigestParamsandPayload. Add it to the import list to avoidIdentifier not found or not uniqueerrors.-import {DigestParams, Payload} from "../../../utils/common/Structs.sol"; +import {DigestParams, Payload, RawPayload} from "../../../utils/common/Structs.sol";
149-187: Honor caller-providedgasLimit.The current
_validateleavesgasLimitat its zero default whenever an override is supplied, so every payload digests withgasLimit == 0. InitializegasLimitfromrawPayload_.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 watcherThis 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
counteris declaredpublicat line 12, Solidity automatically generates acounter()getter. ThisgetCounter()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
counterstate variable (line 10) is overwritten each timedeployCounter()is called. In tests liketestCounterIncrementMultipleChains(lines 62-63), two counters are deployed, but only the last one remains accessible viacounter.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:
- 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; }
- Or use a mapping
mapping(uint32 => Counter) countersto 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 thatexecutePayload()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 valuesOr 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
registerSiblingswalkschainSlugs_but readssiblingPlugs_[i]without checking the arrays are the same size. Any mismatch reverts with a cryptic panic and wastes gas. This should enforcechainSlugs_.length == siblingPlugs_.lengthbefore 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
📒 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
| address plugAddress_, | ||
| bytes memory extraData_ | ||
| ) external view returns (uint64 switchboardId, bytes memory configData) { | ||
| switchboardId = plugSwitchboardIds[plugAddress_]; | ||
| configData = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(plugAddress_, extraData_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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_]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.citeturn0file3
🤖 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.
| function updatePlugConfig(address plug_, bytes memory configData_) external virtual { | ||
| (bytes32 appGatewayId_) = abi.decode(configData_, ( bytes32)); | ||
| plugAppGatewayIds[plug_] = appGatewayId_; | ||
| emit PlugConfigUpdated(plug_, appGatewayId_); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.citeturn0file4turn0file1
🤖 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.
| * @notice Event emitted when plug configuration is updated | ||
| */ | ||
| event PlugConfigUpdated(address indexed plug, bytes configData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| * @notice Event emitted when plug configuration is updated | |
| */ | |
| event PlugConfigUpdated(address indexed plug, bytes configData); | |
| * @notice Event emitted when plug configuration is updated | |
| */ |
| * @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.
| function increase() external onlySocket { | ||
| counter++; | ||
| emit CounterIncreased(counter); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Remove the
value_parameter fromICounterAppGateway.increase()if incrementing by 1 is the intended behavior, or - 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.
| function increase() external onlySocket { | |
| counter++; | |
| emit CounterIncreased(counter); | |
| } | |
| function increase(uint256 value_) external onlySocket { | |
| counter += value_; | |
| emit CounterIncreased(counter); | |
| } |
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
feat: updated payloadId implementation
feat: updated deposit to trigger
There was a problem hiding this 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:
allowPayloadstill uses the legacy(bytes32, bytes32)signature,processPayloadkeeps the removedtriggerId_argument and never returns the required payloadId, and the newincreaseFeesForPayload,updatePlugConfig, andgetPlugConfigoverrides 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 acceptstriggerId_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:
- It's defined in
deprecated/but should be incontracts/utils/common/Structs.sol(whereIAuctionManagerexpects to import it from)- Missing NatSpec documentation
- Unbounded
extraDatafield 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 unboundedextraDataconcern 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
calldatafor 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
RawPayloadas 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:
updatePlugConfignow encodes(chainSlug_, toBytes32Format(siblingPlug_)), but ifFastSwitchboard.updatePlugConfigexpects only a singlebytes32 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=solcontracts/protocol/switchboard/FastSwitchboard.sol (1)
89-92: Fix source decode before comparing appGatewayIdThis should decode the same
(uint32 srcChainSlug, bytes32 appGatewayId)tuple that writers put intoexecuteParams.source. Right nowabi.decode(source_, (bytes32))reads only the first word (the encoded chain slug), soplugAppGatewayIds[target_] != appGatewayIdwill revertInvalidSource()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
_resolvePayloadand_markRevertcallwatcher.resolvePayloadandwatcher.markRevert, which may requireonlyWatcheraccess. 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 viaprecompile.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
blockCreditsafter line 95 and block the computedfeesinstead:- 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.
cancelExecutionsettles withr.maxFees(line 254), charging the full reserved amount even though the payload was never executed. This should settle with0(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 tounblockAndAssignCredits. 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
unblockAndAssignCreditssignature 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'tpure. This will cause a compilation error. Remove thepuremodifier:- function getWatcherAddress() public pure returns (address) { + function getWatcherAddress() public view returns (address) {
294-298: Align _createWatcherSignature mutability.Once
createSignatureis no longerpure(it uses vm.sign), this wrapper can't remainview. Remove theviewmodifier:- ) 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 callswitchboardAddresses[0](address(0)), resulting in a generic "call to non-contract" revert instead of the intendedPlugNotConnected()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
📒 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 stringThe 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
stringtobytesfor 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
updateRequestAndProcessBatchtoupdateRequestis 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 acceptsevmxSlug_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 fromConfigurations. The concern in the original comment is unfounded.Likely an incorrect or invalid review comment.
| 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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
AppGatewayAlreadyCalledandInvalidCallerTriggerederrors 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.
| 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_; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, andlatestAppGateway - 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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))); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.tsLength 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.tsLength 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'.
There was a problem hiding this 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
asynckeyword and writes to an undeclaredoverrideParams, so the contract will not compile. This should dropasync, construct a freshOverrideParamsin memory via the library, and pass that into theRawPayloadbefore 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
_approvehere 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 setsmsg.senderfor the next external call at line 185), but placing it immediately beforewatcher.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
📒 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 todeadline) 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 indicatedwatcher.markRevertisonlyWatcher-gated, but there's no impersonation before the call. The signature-basedWatcherMultiCallParamssuggests 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
createPayloadIdhelper 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:
sourcenow encodes chain and plug information,prevBatchDigestHashno longer uses legacytriggerId, andextraDatamanagement 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
onlyWatchermodifier (which would require hoax) to signature-based validation via_validateSignature(). Watcher.sol'sresolvePayload()now acceptsWatcherMultiCallParamswith 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.
Summary by CodeRabbit
New Features
Refactor
Tests
Documentation