-
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
WalkthroughLarge refactor: moves from a request/credits model to a payload‑first gas model. RequestHandler/FeesManager/Credit removed; Watcher now orchestrates per‑payload lifecycle; new GasAccountManager/GasAccountToken/GasEscrow/GasVault added; payloadId format and Structs changed; Solana (Ed25519/Borsh) and many interfaces/scripts/tests updated. Changes
Sequence Diagram(s)sequenceDiagram
rect rgb(245,250,255)
participant AG as AppGateway
participant W as Watcher
participant P as Precompile
participant G as GasAccountManager
participant S as Socket
end
AG->>W: addPayloadData(RawPayload, appGateway)
W->>W: store payload, set currentPayloadId
W->>P: executePayload() → handlePayload(RawPayload, appGateway, payloadId)
P-->>W: (fees, deadline, precompileData)
W->>G: escrowGas(payloadId, consumeFrom, fees)
W->>S: emit PayloadSubmitted
note right of W: asyncPromise may be deployed/returned
AG->>W: resolvePayload(params)
W->>P: resolvePayload(Payload)
P-->>W: resolution result
W->>G: settleGasPayment(payloadId, transmitter, usedAmount)
W->>S: emit PromiseResolved / PayloadSettled
sequenceDiagram
participant User
participant GM as GasAccountManager
participant GT as GasAccountToken
participant GE as GasEscrow
participant GV as GasVault
User->>GM: wrapToGas() payable
GM->>GT: mint(user, amount)
GM->>GV: forward native -> vault
GM->>GE: escrowGas(payloadId, consumeFrom, amount)
GE->>GE: payloadEscrow[payloadId] = Active
GM->>GE: settleGasPayment(payloadId, transmitter, amount)
GE->>GT: burn(consumeFrom, amount)
GE->>GT: mint(transmitter, amount)
GE->>GE: mark Settled
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes This diff is large, heterogeneous, and introduces new crypto and money flows. Focus extra attention on:
Possibly related PRs
Suggested reviewers
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)); | |
| } |
| /// @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.
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).
contracts/evmx/fees/Credit.sol
Outdated
| 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).
contracts/evmx/fees/FeesManager.sol
Outdated
| /// @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.
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: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)
329-336:setExpiryTimedocs contradict the actual access control.The comment says “Only callable by the contract owner”, but the function is guarded by
onlyWatcher, which allows either the watcher contract or its owner.This should either:
- Be switched to
onlyOwnerif the intent is true owner-only control, or- Have the doc updated to say it is watcher-governed to avoid confusion for integrators and auditors.
18-52: Fix storage layout: gap should be 49 slots, not 50.The declared storage has
watcher__at slot 56, followed by_gap_afterwith 50 slots. This is mathematically inconsistent—the gap comment statesslots [56-105]but slot 56 is occupied bywatcher__. The gap should either be:
- Reduced to 49 slots to span
[57-106], OR- Repositioned with corrected slot numbering
The trailing comment
// 1 slot reserved for watcher baseis also misleading—noWatcherBasecontract exists in the codebase.For upgradeable contracts using proxies, storage layout comments must be precise. Incorrect comments cause deployment failures and make future upgrades error-prone. Update
_gap_aftertouint256[49]and correct the slot comment to// slots [57-106] reserved for gap.
♻️ Duplicate comments (36)
script/counter/IncrementCountersFromApp.s.sol (2)
6-7: Remove unused import/variable and commented-out upload call.
toBytes32Format(Line 7) andcounter(Line 20) are only referenced in the commentedgateway.uploadPlug(...)line (Line 22). This should either be part of the actual script flow (with a brief comment explaining why the plug must be uploaded at runtime) or removed entirely to avoid dead code and unused imports.For removal, this diff is enough:
-import {toBytes32Format} from "../../contracts/utils/common/Converters.sol"; @@ - address counter = vm.envAddress("PLUG"); - - // gateway.uploadPlug(421614, gateway.counter(), toBytes32Format(counter));Also applies to: 20-23
29-33: Fix misleading comment above existence check.The comment on Line 31 says “Count non-zero addresses”, but the code only checks whether
counterForwarderArbitrumSepoliais non-zero and, if so, callsincrementCountersonce. This should describe the existence check instead, e.g. “Check if forwarder is deployed”.script/helpers/TransferRemainingGas.s.sol (1)
36-45: Call the gas token’stransferFrom, notappGateway.call(...).Lines 38–45 build ERC20
transferFromcalldata and send it toappGatewayviaappGateway.call(data). This should invoke the gas token contract instead. As written:
- If
appGatewayis an EOA, the call succeeds but no tokens move.- If
appGatewayis a contract, it reverts unless it happens to implement that signature.This should call
gasAccountToken.transferFrom(appGateway, newAppGateway, availableFees)(or an equivalent SafeERC20 wrapper) and check the boolean return:- uint256 availableFees = gasAccountToken.balanceOf(appGateway); - console.log("Available fees:", availableFees); - bytes memory data = abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - appGateway, - newAppGateway, - availableFees - ); - (bool success, ) = appGateway.call(data); - require(success, "Transfer failed"); + uint256 availableFees = gasAccountToken.balanceOf(appGateway); + console.log("Available fees:", availableFees); + require( + gasAccountToken.transferFrom( + appGateway, + newAppGateway, + availableFees + ), + "Transfer failed" + );Ensure the broadcasting account has the necessary approval/role on
GasAccountTokenso this reverts loudly if misconfigured rather than silently doing nothing.contracts/protocol/switchboard/MessageSwitchboard.sol (2)
138-149:setSiblingConfigshould validate non‑zero sibling socket/switchboard/ID
setSiblingConfigcurrently accepts and stores whatever is passed:function setSiblingConfig( uint32 chainSlug_, bytes32 socket_, bytes32 switchboard_, uint32 switchboardId_ ) external onlyOwner { siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }If
socket_,switchboard_, orswitchboardId_are accidentally zeroed, later calls to_validateSibling/_createDigestAndPayloadIdwill revert withSiblingSocketNotFound, but the root cause is a bad config write.This should treat zero values as invalid (or provide a dedicated “clear config” path), e.g.:
function setSiblingConfig(...) external onlyOwner { + if (socket_ == bytes32(0) || switchboard_ == bytes32(0) || switchboardId_ == 0) { + revert SiblingSocketNotFound(); + } siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }This keeps misconfiguration from being silently recorded.
248-305: Overridedeadlineis decoded but ignored in digest creation
_decodeOverridesderives adeadline(with a default if zero) for both native and sponsored flows:// Version 1 / 2 branches if (deadline == 0) deadline = block.timestamp + defaultDeadline; return MessageOverrides({ ..., gasLimit: gasLimit, deadline: deadline, ... });But
_createDigestAndPayloadIdunconditionally sets:digestParams = DigestParams({ ... deadline: block.timestamp + 3600, ... });
processPayloadnever usesoverrides.deadline, so callers cannot actually control the digest deadline, and thedefaultDeadlineconstant is effectively unused. This was raised in an earlier review and still leaves a misleading API: overrides appear to accept a deadline that does nothing.This should either:
- Wire the override deadline into the digest and validate it, e.g.:
function processPayload(...) external payable override onlySocket returns (bytes32 payloadId) { MessageOverrides memory overrides = _decodeOverrides(overrides_); _validateSibling(overrides.dstChainSlug, plug_); - (DigestParams memory digestParams, bytes32 digest, bytes32 payloadId_) = - _createDigestAndPayloadId(overrides.dstChainSlug, plug_, overrides.gasLimit, overrides.value, payload_); + (DigestParams memory digestParams, bytes32 digest, bytes32 payloadId_) = + _createDigestAndPayloadId( + overrides.dstChainSlug, + plug_, + overrides.gasLimit, + overrides.value, + overrides.deadline, + payload_ + );and adjust
_createDigestAndPayloadIdto takedeadline_and use it, plus a sanity check:- uint256 gasLimit_, - uint256 value_, + uint256 gasLimit_, + uint256 value_, + uint256 deadline_, bytes calldata payload_ ) internal returns (DigestParams memory digestParams, bytes32 digest, bytes32 payloadId) { ... - digestParams = DigestParams({ + if (deadline_ <= block.timestamp) revert DeadlinePassed(); + digestParams = DigestParams({ ... - deadline: block.timestamp + 3600, + deadline: deadline_, ... });
- Or, if deadline is truly not part of switchboard semantics, remove
deadlinefromMessageOverridesand the override encoding/decoding to avoid exposing a non-functional field.As it stands, override callers can encode any deadline but the actual digest always uses a fixed 1‑hour window.
Also applies to: 320-345
contracts/evmx/interfaces/IPromise.sol (1)
21-22: Breaking API change already flagged in previous review.The migration from
requestCount()returninguint40topayloadId()returningbytes32is a breaking change. A previous review already identified that active contracts (AsyncDeployer, IAsyncDeployer, AppGatewayBase) still referencerequestCount.contracts/evmx/interfaces/IConfigurations.sol (1)
48-48: Use calldata for external struct parameter.A previous review correctly identified that
WatcherMultiCallParams memory params_should usecalldatato avoid unnecessary copies and enforce ABI consistency.contracts/evmx/helpers/solana-utils/Ed25519_pow.sol (1)
4-328: Magic number repeated 200+ times bloats bytecode.A previous review correctly identified that the Ed25519 field prime
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedis hardcoded throughout the function, significantly increasing bytecode size and reducing maintainability.hardhat-scripts/deploy/4.configureEVMx.ts (1)
69-78: DeployForwarder configuration is still disabled; either wire it or document why it is skippedThe entire
setDeployForwarderupdateContractSettingsblock is commented out. This leavesdeployForwarder__unset in AddressResolver unless there is an alternative wiring path (none is shown here).This should either:
- Be re-enabled so DeployForwarder is configured alongside AsyncDeployer/Watcher, or
- Be explicitly documented with a code comment explaining why DeployForwarder is intentionally not configured here and where it is configured instead.
Leaving this silently disabled risks runtime misconfiguration.
script/helpers/DepositGasAndNative.s.sol (1)
12-34: Forward native value when callingdepositGasTokenAndNativeThe script never sends any native ETH with the
depositGasTokenAndNativecall:gasStation.depositGasTokenAndNative(address(testUSDCContract), appGateway, feesAmount);If
depositGasTokenAndNativeexpectsmsg.valueto carry the native top-up, this call will either revert or skip native funding despite the script’s name implying a combined token+native deposit.This should load the native amount from config and forward it as call value, e.g.:
- uint256 feesAmount = 100000000; // 100 USDC + uint256 feesAmount = 100000000; // 100 USDC + uint256 nativeAmount = vm.envUint("NATIVE_AMOUNT_WEI"); // or a hardcoded test value ... - gasStation.depositGasTokenAndNative(address(testUSDCContract), appGateway, feesAmount); + gasStation.depositGasTokenAndNative{value: nativeAmount}( + address(testUSDCContract), + appGateway, + feesAmount + );Adjust the env var name and units to whatever your deployment uses, but the native value forwarding should be explicit.
#!/bin/bash # Inspect GasStation ABI to confirm depositGasTokenAndNative's expectations rg -n "depositGasTokenAndNative" -S contracts -C3contracts/evmx/helpers/solana-utils/Ed25519.sol (1)
8-24: Fixbytes32misuse when building(R, A, M)buffer; current code will not compile
kandrarebytes32, but the code treats them like dynamic bytes:bytes memory rs = new bytes(k.length + r.length + m.length); for (uint256 i = 0; i < r.length; i++) { rs[i] = r[i]; } for (uint256 i = 0; i < k.length; i++) { rs[i + 32] = k[i]; }
bytes32has no.lengthand cannot be indexed this way, so this will not compile and the entire verifier is unusable.This should construct
rsas a 32+32+len(m) byte sequence using valid operations. The simplest and safest fix is:- { - bytes memory rs = new bytes(k.length + r.length + m.length); - for (uint256 i = 0; i < r.length; i++) { - rs[i] = r[i]; - } - for (uint256 i = 0; i < k.length; i++) { - rs[i + 32] = k[i]; - } - for (uint256 i = 0; i < m.length; i++) { - rs[i + 64] = m[i]; - } - uint64[8] memory result = Sha512.hash(rs); - ... - } + { + // rs = R (32 bytes) || A (32 bytes) || M + bytes memory rs = abi.encodePacked(r, k, m); + uint64[8] memory result = Sha512.hash(rs); + ... + }This preserves the intended
(R, A, M)concatenation while using legal and more gas-efficient encoding.contracts/evmx/helpers/AsyncDeployer.sol (1)
34-40: Fix storage gap comment to reflect new layoutWith
defaultDeadlinenow occupying slot 55,_gap_afterspans slots 56–105 (50 slots), not 55–104.The comment on Line 37 should be updated accordingly so the documented layout matches the actual one and future upgrades don’t rely on stale indices.
This should read:
// slots [56-105] reserved for gap uint256[50] _gap_after;contracts/evmx/helpers/AddressResolverUtil.sol (2)
33-38: Fix misleading comment.The comment says "Restricts function access to the watcher owner" but
isWatcher()returns true for either the watcher contract OR the watcher owner. This is misleading.Apply this diff:
- /// @notice Restricts function access to the watcher owner + /// @notice Checks if caller is the watcher contract or watcher owner function isWatcher() internal view returns (bool) {
47-73: Fix mismatched NatDoc comments.The NatDoc comments are shifted—each describes the previous function instead of the current one:
- Line 47-49: Says "watcher precompile" but function is
gasAccountManager__()- Line 54-56: Says "gas account manager" but function is
gasVault__()- Line 61-63: Says "gas vault" but function is
gasEscrow__()- Line 68-70: Says "gas escrow" but function is
gasAccountToken__()Apply this diff:
- /// @notice Gets the watcher precompile contract interface - /// @return IWatcher interface of the registered watcher precompile - /// @dev Resolves and returns the watcher precompile contract for interaction + /// @notice Gets the gas account manager contract interface + /// @return IGasAccountManager interface of the registered gas account manager + /// @dev Resolves and returns the gas account manager contract for interaction function gasAccountManager__() public view returns (IGasAccountManager) { return addressResolver__.gasAccountManager__(); } - /// @notice Gets the gas account manager contract interface - /// @return IGasAccountManager interface of the registered gas account manager - /// @dev Resolves and returns the gas account manager contract for interaction + /// @notice Gets the gas vault contract interface + /// @return IGasVault interface of the registered gas vault + /// @dev Resolves and returns the gas vault contract for interaction function gasVault__() public view returns (IGasVault) { return addressResolver__.gasVault__(); } - /// @notice Gets the gas vault contract interface - /// @return IGasVault interface of the registered gas vault - /// @dev Resolves and returns the gas vault contract for interaction + /// @notice Gets the gas escrow contract interface + /// @return IGasEscrow interface of the registered gas escrow + /// @dev Resolves and returns the gas escrow contract for interaction function gasEscrow__() public view returns (IGasEscrow) { return addressResolver__.gasEscrow__(); } - /// @notice Gets the gas escrow contract interface - /// @return IGasEscrow interface of the registered gas escrow - /// @dev Resolves and returns the gas escrow contract for interaction + /// @notice Gets the gas account token contract interface + /// @return IGasAccountToken interface of the registered gas account token + /// @dev Resolves and returns the gas account token contract for interaction function gasAccountToken__() public view returns (IGasAccountToken) { return addressResolver__.gasAccountToken__(); }contracts/evmx/fees/GasEscrow.sol (1)
78-94: State machine allows mixing settle and release operations.The TODO on line 79 flags a real issue. After a partial
settleGasPayment(line 80-94), the entry remains inActivestate (line 92 only transitions toSettledwhenentry.amount == 0). This allowsreleaseEscrowto be called afterward since it only checksstate != Active(line 71).The problem: once settlement begins, release should be blocked. The escrow should follow one path (settle to completion) or the other (release), not both.
Fix by transitioning to
Settledon first settle call, or add aPartiallySettledstate:function settleGasPayment( bytes32 payloadId, address transmitter, uint256 amount ) external onlyGasAccountManager { EscrowEntry storage entry = payloadEscrow[payloadId]; if (entry.state != EscrowState.Active) revert NotActive(); if (entry.amount == 0) revert NoEscrow(); accountEscrow[entry.account] -= amount; entry.amount -= amount; - if (entry.amount == 0) entry.state = EscrowState.Settled; + entry.state = EscrowState.Settled; // Block release after ANY settle emit EscrowSettled(payloadId, entry.account, transmitter, amount); }Then update
releaseEscrowto explicitly rejectSettledstate if needed.contracts/evmx/base/AppGatewayBase.sol (2)
33-33: Invalid override on state variable.The override modifier is not valid on public state variables in Solidity. This should compile as a plain public mapping.
- mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses; + mapping(bytes32 => mapping(uint32 => address)) public forwarderAddresses;
62-71: Async guard not reset and missing zero-address check.isAsyncModifierSet is set true in _preAsync (line 64) but never cleared in _postAsync. This allows setSchedule to be called outside the intended async scope. Also missing zero-address validation for promise.
function _postAsync() internal { _clearOverrides(); address promise_ = watcher__().executePayload(); + if (promise_ != address(0)) { isValidPromise[promise_] = true; + } + // Close the async window + isAsyncModifierSet = false; }contracts/evmx/fees/GasAccountToken.sol (2)
63-66: Add underflow protection to balanceOf.Direct subtraction will revert if getEscrowedAmount exceeds the parent balance due to any escrow accounting bug, breaking all balance queries and potentially locking funds.
Apply this diff:
function balanceOf(address account) public view override returns (uint256) { + uint256 total = super.balanceOf(account); + uint256 escrowed = addressResolver__.gasEscrow__().getEscrowedAmount(account); + if (escrowed > total) return 0; - return - super.balanceOf(account) - addressResolver__.gasEscrow__().getEscrowedAmount(account); + return total - escrowed; }
81-90: Watcher auto-approval bypasses ERC20 security model.Lines 87-88 automatically approve the watcher to transfer any amount from any account. If the watcher is compromised, all SGAS balances are at risk. This violates the principle of least privilege and standard ERC20 expectations.
Consider requiring explicit user approval or implementing a separate escrow-transfer path that doesn't use the standard ERC20 transferFrom flow.
contracts/evmx/helpers/AsyncPromise.sol (2)
26-28: CRITICAL: Storage layout break.Replacing the packed uint40 requestCount with bytes32 payloadId in-place shifts all subsequent variables into new slots, corrupting state on upgrades. This must be fixed before merge.
Preserve the original packed layout and append payloadId at the end:
- /// @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 (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;
84-94: Store the absolute deadline.Line 93 adds block.timestamp to deadline_, treating it as a relative offset. If the watcher passes an absolute deadline, this will incorrectly inflate the deadline and DeadlinePassed checks won't trigger on time.
- promiseDeadline = deadline_ + block.timestamp; + promiseDeadline = deadline_;contracts/evmx/helpers/AddressResolver.sol (1)
29-33: Storage slot comment is still incorrect.
gasAccountToken__already occupies slot 55, socontractAddressesstarts at slot 56. The comment should reflect that to avoid future upgrade/migration mistakes in this storage-heavy contract.This should update the comment as:
- // slot 55 + // slot 56 mapping(bytes32 => address) public override contractAddresses;contracts/evmx/watcher/precompiles/WritePrecompile.sol (2)
8-17: Missing imports forRawPayloadandSolanaInstructionwill stop compilation.This file uses
RawPayload(event and function signatures) andSolanaInstruction(abi.decodeandBorshEncoder.encodeFunctionArgs) but does not import them fromStructs.sol. The compiler will fail to resolve these types.This should extend the Structs import as follows:
-import {DigestParams, Payload} from "../../../utils/common/Structs.sol"; +import {DigestParams, Payload, RawPayload, SolanaInstruction} from "../../../utils/common/Structs.sol";
160-200:_validatestill ignores non‑zerooverrideParams.gasLimitand returns 0.When a caller sets
rawPayload_.overrideParams.gasLimit > 0, this function never assigns that value togasLimit. As a result,handlePayloadpropagates a zero gas limit intoprecompileDataand the digest, while the intended override is lost. This breaks digest correctness and downstream gas accounting.This should initialize
gasLimitfrom the override, and only derive chain defaults when it is zero:function _validate( RawPayload calldata rawPayload_, address appGateway_ ) internal view returns (uint256 gasLimit) { + // Start with caller-provided override + gasLimit = rawPayload_.overrideParams.gasLimit; @@ - // todo: can be changed to set the default gas limit for each chain - if (rawPayload_.overrideParams.gasLimit == 0) { + // todo: can be changed to set the default gas limit for each chain + 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/plugs/GasStation.sol (1)
13-20: Simplify payloadId return type to bytes32 and drop dynamic decode.
IGasAccountManager.depositFromChainreturnsbytes memory, but_depositimmediatelyabi.decodes it intobytes32 payloadId. If the manager always returns a singlebytes32payloadId, this should use abytes32return type end‑to‑end and avoid the dynamic allocation/extra decode. It also keeps payloadId typing consistent across the codebase and reduces the chance of ABI mismatches between manager and caller.This should:
- Change the interface to
returns (bytes32 payloadId)and- Update
_depositto take thebytes32directly (noabi.decode).If there is a shared
IGasAccountManagerinterface undercontracts/evmx/interfaces, this should be updated there and imported here instead of re‑declaring it.Also applies to: 79-101
contracts/protocol/switchboard/FastSwitchboard.sol (1)
124-135: Guard against accidentally zeroing the EVMX config.
setEvmxConfigaccepts zero forevmxChainSlug_orwatcherId_, butprocessPayloadtreats any zero as “config not set” and reverts withEvmxConfigNotSet(). A single bad governance call can silently disable all payload processing until another transaction fixes it.This should either:
- Reject zero values in
setEvmxConfig:function setEvmxConfig(uint32 evmxChainSlug_, uint32 watcherId_) external onlyOwner { + if (evmxChainSlug_ == 0 || watcherId_ == 0) revert EvmxConfigNotSet(); evmxChainSlug = evmxChainSlug_; watcherId = watcherId_; emit EvmxConfigSet(evmxChainSlug_, watcherId_); }
- Or introduce a dedicated
clearEvmxConfigfunction if an explicit “off” switch is desired, and document that behavior.Also applies to: 146-153
contracts/protocol/SocketConfig.sol (2)
98-108: Validate network fee collector address on set.
setNetworkFeeCollectorcurrently trusts anynetworkFeeCollector_and casts it toINetworkFeeCollectorwithout checks. A misconfigured EOA or wrong contract will only show up later as opaque call failures when fees are collected.This should:
- Allow
address(0)explicitly for “fee collection disabled” if desired, but- When non‑zero, assert that
networkFeeCollector_.code.length > 0to guarantee it is a contract.For example:
function setNetworkFeeCollector( address networkFeeCollector_ ) external onlyRole(GOVERNANCE_ROLE) { - emit NetworkFeeCollectorUpdated(address(networkFeeCollector), networkFeeCollector_); - networkFeeCollector = INetworkFeeCollector(networkFeeCollector_); + if (networkFeeCollector_ != address(0) && networkFeeCollector_.code.length == 0) { + revert InvalidContract(); + } + emit NetworkFeeCollectorUpdated(address(networkFeeCollector), networkFeeCollector_); + networkFeeCollector = INetworkFeeCollector(networkFeeCollector_); }This makes misconfiguration fail fast instead of at first use.
179-188: Avoid calling switchboard at address(0) for disconnected plugs.
getPlugConfigreadsswitchboardId = plugSwitchboardIds[plugAddress_]and immediately callsISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig(...). When a plug is not connected (switchboardId == 0), this ends up callinggetPlugConfigonaddress(0)and produces a generic low‑level revert instead of the intendedPlugNotConnected()signal.This should explicitly guard the zero case before touching the switchboard:
function getPlugConfig( address plugAddress_, bytes memory extraData_ ) external view returns (uint32 switchboardId, bytes memory plugConfig) { switchboardId = plugSwitchboardIds[plugAddress_]; + if (switchboardId == 0) revert PlugNotConnected(); - plugConfig = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig( - plugAddress_, - extraData_ - ); + address switchboard = switchboardAddresses[switchboardId]; + plugConfig = ISwitchboard(switchboard).getPlugConfig(plugAddress_, extraData_); }This keeps the error surface intentional and avoids confusing “call to non‑contract” failures.
contracts/evmx/watcher/Configurations.sol (1)
36-38: EnsureevmxSlugis explicitly initialized before using it in signature digests.
_validateSignaturehashesabi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_), but this abstract contract never setsevmxSlug. If no derived contract writes it, the domain is permanentlyevmxSlug == 0, which will either break signature validation or silently tie all signatures to a zero slug.This should provide a clear initialization path (and document it), for example:
- Add an
onlyOwnersetter onConfigurations:+ event EvmxSlugSet(uint32 evmxSlug); + + function setEvmxSlug(uint32 evmxSlug_) external onlyOwner { + evmxSlug = evmxSlug_; + emit EvmxSlugSet(evmxSlug_); + }
- Or, if a derived contract is already setting
evmxSlug, document that requirement and ensure it runs before any call that eventually uses_validateSignature.#!/bin/bash # Verify that evmxSlug is assigned somewhere in the repo (constructor or setter). rg -n 'evmxSlug\s*=' -SAlso applies to: 158-180
contracts/evmx/fees/GasAccountManager.sol (2)
139-164: Clarify and align bridge fee consumption inwithdrawToChain
withdrawToChainenforces:uint256 gasBalance = gasAccountToken__().balanceOf(consumeFrom); if (gasBalance < amount + bridgeFee) revert InsufficientGasAvailable(); // Burn tokens from sender gasAccountToken__().burn(consumeFrom, amount); tokenOnChainBalances[chainSlug][toBytes32Format(token)] -= amount;You verify
amount + bridgeFeeis available but only burnamounthere. From this function’s local view, thebridgeFeeportion is never consumed. It appears you intend the fee to be charged later viaescrowGas/settleGasPaymenton the Watcher path, but that coupling is not obvious.This should either:
- Burn or escrow the
bridgeFeeportion here (so the pre‑check and consumption match), or- Add an explicit comment referencing the later escrow/settlement flow so readers know where the fee is actually consumed.
Right now the mismatch between the check and immediate burn is misleading and was previously flagged as a concern.
96-107: Use escrow‑aware gas availability checks instead of rawbalanceOfBoth
unwrapFromGas,escrowGas, andwithdrawToChainusegasAccountToken__().balanceOf(...)to decide whether sufficient gas is available:if (gasAccountToken__().balanceOf(msg.sender) < amount) revert InsufficientGasAvailable(); ... if (gasAccountToken__().balanceOf(account) < amount) revert InsufficientGasAvailable(); ... uint256 gasBalance = gasAccountToken__().balanceOf(consumeFrom); if (gasBalance < amount + bridgeFee) revert InsufficientGasAvailable();If
GasAccountTokentracks escrowed balances separately (whichisGasAvailableis meant to handle), these checks can overestimate what the user can actually spend, allowing over‑escrow or unwrap beyond free balance. The TODO comment acknowledges this.This should consistently use the escrow‑aware helper (e.g.
isGasAvailable(...)) in all three places, and the TODO should be removed once done. This aligns behavior with how other components (e.g. Watcher) gate gas usage.Also applies to: 193-196, 149-152
contracts/evmx/watcher/Watcher.sol (5)
233-252: Deadline validation in_callAppGatewaysshould bound overly long deadlines
_callAppGatewaysdecodes a deadline and only checks that it is not in the past:uint256 deadline = abi.decode(params_.overrides, (uint256)); if (deadline < block.timestamp) revert DeadlinePassed();This allows callers to set arbitrarily far‑future deadlines (e.g.
type(uint256).max), effectively disabling timeout for triggers.This should bound deadlines to a reasonable window (or to a configured maximum), for example:
uint256 deadline = abi.decode(params_.overrides, (uint256)); if (deadline > block.timestamp + MAX_DEADLINE_WINDOW) revert InvalidDeadline(); if (deadline < block.timestamp) revert DeadlinePassed();with
MAX_DEADLINE_WINDOWdefined in configuration. That keeps trigger lifetimes sane and prevents unbounded outstanding work.
64-79: addPayloadData has no access control and uses shared mutable state, enabling races
addPayloadDataisexternaland unrestricted, and it writes to shared state (payloadData,currentPayloadId,latestAsyncPromise,latestAppGateway) that is later read byexecutePayload:function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) external { if (payloadData.asyncPromise != address(0)) revert PayloadAlreadySet(); payloadData = rawPayload_; currentPayloadId = getCurrentPayloadId(...); latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract(appGateway_, currentPayloadId); latestAppGateway = appGateway_; }Issues:
No access control – Any external caller can overwrite
payloadData,currentPayloadId, andlatestAppGateway, and force deployment of arbitrary async promises. This should be restricted (e.g. to a trusted app gateway or watcher via a modifier / role).Race / overwrite – The guard uses
payloadData.asyncPromiseas a sentinel, but this contract never sets that field, so it stays zero and does not prevent multiple concurrentaddPayloadDatacalls. Multiple calls beforeexecutePayloadwill overwrite the prior pending payload, andexecutePayloadwill operate only on the last one.This should:
- Add strict access control to
addPayloadData(e.g.onlyWatcheror an explicit appGateway allowlist), and- Either make the submit+execute flow atomic (single external entry) or key temporary state per payloadId instead of using one global slot.
Also applies to: 21-37
81-122: Watcher can only safely handle one payload at a time due to global temp state
executePayloadconsumes the globalpayloadData,currentPayloadId,latestAsyncPromise, andlatestAppGateway:function executePayload() external whenNotPaused returns (address asyncPromise) { if (latestAppGateway != msg.sender) revert AppGatewayMismatch(); ... gasAccountManager__().escrowGas(currentPayloadId, ... maxFees); asyncPromise = latestAsyncPromise; ... _payloads[currentPayloadId] = Payload({ ... payloadPointer: nextPayloadCount++, ... }); emit PayloadSubmitted(_payloads[currentPayloadId]); }Because these are single shared variables, multiple outstanding payloads cannot be represented safely:
- Every
addPayloadDatacall overwrites these fields.executePayloadnever resets them after use.- There is no mapping from payloadId → RawPayload for later execution; only the last staged payload is visible.
This effectively limits the watcher to a single in‑flight payload and makes behavior undefined if
addPayloadDatais called again beforeexecutePayloadcompletes for the previous payload. This should be refactored so temporary data is keyed per payload (e.g. map bypayloadIdor index) or the staging+execution are combined into a single external call.Also applies to: 31-35
233-262: Missing import forfromBytes32Formatin_callAppGateways(compile‑blocker)
_callAppGatewaysusesfromBytes32Format:address appGateway = fromBytes32Format(params_.appGatewayId);but this file does not import
Converters.sol, andConfigurations.solwill not re‑export free functions from its own imports. Unless another imported unit in this file explicitly definesfromBytes32Formatin scope, this will not compile.This should add the explicit import:
import {fromBytes32Format} from "../../utils/common/Converters.sol";(or the appropriate named import from your Converters library) so
_callAppGatewayshas access to the symbol.
285-305: Cancellation path always charges fullmaxFees, even if no fees were used
cancelExecutionsettles fees using the fullr.maxFees:function cancelExecution(bytes32 payloadId_) public { Payload storage r = _payloads[payloadId_]; if (r.isPayloadExecuted) revert PayloadAlreadySettled(); if (r.isPayloadCancelled) revert PayloadAlreadyCancelled(); r.isPayloadCancelled = true; r.isTransmitterFeesSettled = true; gasAccountManager__().settleGasPayment( payloadId_, r.consumeFrom, transmitter, r.maxFees - r.watcherFees ); gasAccountManager__().settleGasPayment( payloadId_, r.consumeFrom, address(this), r.watcherFees ); emit PayloadCancelled(payloadId_); }Combined with the earlier escrow of
maxFeesand the resolution path in_resolvePayload, this means:
- On normal resolution, you settle using the actual
feesUsed_andp.watcherFees, then release escrow.- On cancellation (including via
_markRevertonce deadline passes), you always burn the entiremaxFeesfrom the user (split between transmitter and watcher), regardless of how much gas was actually spent.This should track and settle against actual fees used, not the full reserved max, for cancellation and revert scenarios; otherwise users are charged the worst‑case fee even when nothing was executed. A safer pattern:
- Track
feesUsedin the payload struct as it accrues.- On cancellation, settle with the recorded
feesUsedand refund any remaining escrow viareleaseEscrow.If you intend to charge full
maxFeeson cancellation, this should be explicitly documented in comments and user‑facing docs, since it is a non‑obvious and expensive behavior.Also applies to: 136-171, 205-221
| .idea/ | ||
|
|
||
| hardhat-scripts/loadTest/* | ||
| hardhat-scripts/loadTest/* |
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 the directory ignore pattern.
The pattern hardhat-scripts/loadTest/* only ignores files that are direct children of the directory. Subdirectories and their contents won't be ignored. Use hardhat-scripts/loadTest/ to properly ignore the entire directory.
-hardhat-scripts/loadTest/*
+hardhat-scripts/loadTest/📝 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.
| hardhat-scripts/loadTest/* | |
| hardhat-scripts/loadTest/ |
🤖 Prompt for AI Agents
In .gitignore at line 39, the pattern "hardhat-scripts/loadTest/*" only ignores
files directly inside that folder but not nested directories; replace it with
"hardhat-scripts/loadTest/" (i.e., remove the trailing asterisk and keep the
trailing slash) so the entire directory and all subdirectories/files are
ignored, then save the .gitignore and verify with git status or git check-ignore
to confirm the path is now excluded.
| function unwrapFromGas(uint256 amount, address receiver) external { | ||
| // todo: use isGasAvailable, check all gasAccountToken__().balanceOf instances | ||
| if (gasAccountToken__().balanceOf(msg.sender) < amount) revert InsufficientGasAvailable(); | ||
|
|
||
| // Burn tokens from sender | ||
| gasAccountToken__().burn(msg.sender, amount); | ||
|
|
||
| bool success = gasVault__().withdraw(receiver, amount); | ||
| if (!success) revert InsufficientBalance(); | ||
|
|
||
| emit GasUnwrapped(msg.sender, 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.
Validate receiver address on unwrap to avoid burning to zero
unwrapFromGas does not validate receiver:
function unwrapFromGas(uint256 amount, address receiver) external {
...
bool success = gasVault__().withdraw(receiver, amount);
if (!success) revert InsufficientBalance();
}This should reject receiver == address(0) to avoid accidentally burning native assets to the zero address when unwrapping. Adding a simple check keeps unwrap behavior symmetric with wrap and avoids silent loss of funds due to caller mistakes:
if (receiver == address(0)) revert InvalidReceiver();🤖 Prompt for AI Agents
In contracts/evmx/fees/GasAccountManager.sol around lines 96-107, add a guard
that rejects receiver == address(0) before performing the burn/withdraw to
prevent funds being sent to the zero address; implement this by checking the
receiver and reverting with InvalidReceiver() (or create that error if missing)
immediately at the start of unwrapFromGas, then proceed with the existing
balance checks, burn, withdraw and emit to keep behavior symmetric with wrap;
update or add tests to cover the zero-address unwrap case.
| function initialize(address owner_, address addressResolver_) external reinitializer(1) { | ||
| addressResolver__ = IAddressResolver(addressResolver_); | ||
| _setOwner(owner_); | ||
| } |
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.
Add zero-address validation for addressResolver_.
The initialize function accepts addressResolver_ without validating it's non-zero. If initialized with address(0), all calls to gasAccountManager__(), gasEscrow__(), and watcher__() will fail when attempting to resolve addresses.
Apply this diff:
function initialize(address owner_, address addressResolver_) external reinitializer(1) {
+ require(addressResolver_ != address(0), "Invalid address resolver");
addressResolver__ = IAddressResolver(addressResolver_);
_setOwner(owner_);
}🤖 Prompt for AI Agents
In contracts/evmx/fees/GasAccountToken.sol around lines 36 to 39, the initialize
function does not validate addressResolver_ and can be called with address(0);
add a non-zero check at the start of initialize (e.g., require(addressResolver_
!= address(0), "GasAccountToken: zero addressResolver")) before assigning
addressResolver__ and _setOwner to prevent later resolution failures when
gasAccountManager__(), gasEscrow__(), or watcher__() are invoked.
| uint256 deadline; | ||
| address sponsor; | ||
| address transmitter; | ||
| uint256 feeAmount; | ||
| uint256 nonce; | ||
| } | ||
|
|
||
| // Struct to store message details | ||
| struct MessageDetails { | ||
| uint32 srcChainSlug; | ||
| uint32 dstChainSlug; | ||
| bytes32 srcPlug; | ||
| bytes32 dstPlug; | ||
| uint256 deadline; | ||
| address sponsor; | ||
| address transmitter; | ||
| uint256 feeAmount; | ||
| MessageStatus status; | ||
| } |
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.
deadline is stored but never enforced in execution flow
MessageDetailsInput and MessageDetails both carry a deadline:
struct MessageDetailsInput {
...
uint256 deadline;
...
}
struct MessageDetails {
...
uint256 deadline;
...
}addMessageDetails persists input_.deadline, but markExecuted never reads details.deadline when deciding whether to pay the transmitter. As written, a watcher can mark a message as executed and trigger payment long after the nominal deadline.
If deadline is meant to be an expiry/TTL for payment, this should enforce it, e.g.:
function markExecuted(bytes32 payloadId_, uint256 nonce_, bytes calldata signature_) external {
MessageDetails storage details = messageDetails[payloadId_];
if (details.status == MessageStatus.NotAdded) revert MessageNotFound();
if (details.status != MessageStatus.Pending) revert MessageNotPending();
+ if (block.timestamp > details.deadline) revert DeadlinePassed(); // or a resolver-specific error
...
}If expiry is intentionally enforced only off‑chain, deadline should be removed from on‑chain storage and API to avoid a misleading field that appears to be enforced but is not.
Also applies to: 210-277
| function decodeGenericSchema( | ||
| GenericSchema memory schema, | ||
| bytes memory encodedData | ||
| ) internal pure returns (bytes[] memory) { | ||
| bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length); | ||
| Data memory data = from(encodedData); | ||
|
|
||
| for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) { | ||
| string memory typeName = schema.valuesTypeNames[i]; | ||
|
|
||
| if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { | ||
| uint8 value = data.decodeU8(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { | ||
| uint16 value = data.decodeU16(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { | ||
| uint32 value = data.decodeU32(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { | ||
| uint64 value = data.decodeU64(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { | ||
| uint128 value = data.decodeU128(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { | ||
| string memory value = data.decodeString(); | ||
| decodedParams[i] = abi.encode(value); | ||
| } | ||
| // Handle Vector types with variable length | ||
| else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u8>"))) { | ||
| uint32 length; | ||
| uint8[] memory value; | ||
| (length, value) = decodeUint8Vec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) { | ||
| uint32 length; | ||
| uint16[] memory value; | ||
| (length, value) = decodeUint16Vec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) { | ||
| uint32 length; | ||
| uint32[] memory value; | ||
| (length, value) = decodeUint32Vec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) { | ||
| uint32 length; | ||
| uint64[] memory value; | ||
| (length, value) = decodeUint64Vec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) { | ||
| uint32 length; | ||
| uint128[] memory value; | ||
| (length, value) = decodeUint128Vec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) { | ||
| uint32 length; | ||
| string[] memory value; | ||
| (length, value) = decodeStringVec(data); | ||
| decodedParams[i] = abi.encode(value); | ||
| } | ||
| // Handle Array types with fixed length | ||
| else if (BorshUtils.startsWith(typeName, "[u8;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| uint8[] memory value = decodeUint8Array(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (BorshUtils.startsWith(typeName, "[u16;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| uint16[] memory value = decodeUint16Array(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (BorshUtils.startsWith(typeName, "[u32;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| uint32[] memory value = decodeUint32Array(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (BorshUtils.startsWith(typeName, "[u64;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| uint64[] memory value = decodeUint64Array(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (BorshUtils.startsWith(typeName, "[u128;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| uint128[] memory value = decodeUint128Array(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else if (BorshUtils.startsWith(typeName, "[String;")) { | ||
| uint256 length = BorshUtils.extractArrayLength(typeName); | ||
| string[] memory value = decodeStringArray(data, length); | ||
| decodedParams[i] = abi.encode(value); | ||
| } else { | ||
| revert("Unsupported type"); | ||
| } | ||
| } | ||
|
|
||
| return decodedParams; | ||
| } |
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.
Enforce full input consumption in decodeGenericSchema to catch schema mismatches
decodeGenericSchema walks the schema and advances the Data cursor, but it never calls data.done(). Extra trailing bytes in encodedData will be silently ignored, which can hide schema/encoding mismatches and make cross-chain debugging harder.
This should assert full consumption at the end:
function decodeGenericSchema(
GenericSchema memory schema,
bytes memory encodedData
) internal pure returns (bytes[] memory) {
bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length);
Data memory data = from(encodedData);
for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) {
string memory typeName = schema.valuesTypeNames[i];
...
}
-
- return decodedParams;
+ data.done();
+ return decodedParams;
}Optionally, this function should also cache keccak256(bytes(typeName)) once per loop iteration instead of recomputing it for every branch, to cut gas when decoding many fields.
📝 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 decodeGenericSchema( | |
| GenericSchema memory schema, | |
| bytes memory encodedData | |
| ) internal pure returns (bytes[] memory) { | |
| bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length); | |
| Data memory data = from(encodedData); | |
| for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) { | |
| string memory typeName = schema.valuesTypeNames[i]; | |
| if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { | |
| uint8 value = data.decodeU8(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { | |
| uint16 value = data.decodeU16(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { | |
| uint32 value = data.decodeU32(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { | |
| uint64 value = data.decodeU64(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { | |
| uint128 value = data.decodeU128(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { | |
| string memory value = data.decodeString(); | |
| decodedParams[i] = abi.encode(value); | |
| } | |
| // Handle Vector types with variable length | |
| else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u8>"))) { | |
| uint32 length; | |
| uint8[] memory value; | |
| (length, value) = decodeUint8Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) { | |
| uint32 length; | |
| uint16[] memory value; | |
| (length, value) = decodeUint16Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) { | |
| uint32 length; | |
| uint32[] memory value; | |
| (length, value) = decodeUint32Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) { | |
| uint32 length; | |
| uint64[] memory value; | |
| (length, value) = decodeUint64Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) { | |
| uint32 length; | |
| uint128[] memory value; | |
| (length, value) = decodeUint128Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) { | |
| uint32 length; | |
| string[] memory value; | |
| (length, value) = decodeStringVec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } | |
| // Handle Array types with fixed length | |
| else if (BorshUtils.startsWith(typeName, "[u8;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint8[] memory value = decodeUint8Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u16;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint16[] memory value = decodeUint16Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u32;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint32[] memory value = decodeUint32Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u64;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint64[] memory value = decodeUint64Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u128;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint128[] memory value = decodeUint128Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[String;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| string[] memory value = decodeStringArray(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else { | |
| revert("Unsupported type"); | |
| } | |
| } | |
| return decodedParams; | |
| } | |
| function decodeGenericSchema( | |
| GenericSchema memory schema, | |
| bytes memory encodedData | |
| ) internal pure returns (bytes[] memory) { | |
| bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length); | |
| Data memory data = from(encodedData); | |
| for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) { | |
| string memory typeName = schema.valuesTypeNames[i]; | |
| if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { | |
| uint8 value = data.decodeU8(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { | |
| uint16 value = data.decodeU16(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { | |
| uint32 value = data.decodeU32(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { | |
| uint64 value = data.decodeU64(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { | |
| uint128 value = data.decodeU128(); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { | |
| string memory value = data.decodeString(); | |
| decodedParams[i] = abi.encode(value); | |
| } | |
| // Handle Vector types with variable length | |
| else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u8>"))) { | |
| uint32 length; | |
| uint8[] memory value; | |
| (length, value) = decodeUint8Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) { | |
| uint32 length; | |
| uint16[] memory value; | |
| (length, value) = decodeUint16Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) { | |
| uint32 length; | |
| uint32[] memory value; | |
| (length, value) = decodeUint32Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) { | |
| uint32 length; | |
| uint64[] memory value; | |
| (length, value) = decodeUint64Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) { | |
| uint32 length; | |
| uint128[] memory value; | |
| (length, value) = decodeUint128Vec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) { | |
| uint32 length; | |
| string[] memory value; | |
| (length, value) = decodeStringVec(data); | |
| decodedParams[i] = abi.encode(value); | |
| } | |
| // Handle Array types with fixed length | |
| else if (BorshUtils.startsWith(typeName, "[u8;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint8[] memory value = decodeUint8Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u16;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint16[] memory value = decodeUint16Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u32;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint32[] memory value = decodeUint32Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u64;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint64[] memory value = decodeUint64Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[u128;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| uint128[] memory value = decodeUint128Array(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else if (BorshUtils.startsWith(typeName, "[String;")) { | |
| uint256 length = BorshUtils.extractArrayLength(typeName); | |
| string[] memory value = decodeStringArray(data, length); | |
| decodedParams[i] = abi.encode(value); | |
| } else { | |
| revert("Unsupported type"); | |
| } | |
| } | |
| data.done(); | |
| return decodedParams; | |
| } |
🤖 Prompt for AI Agents
In contracts/evmx/watcher/borsh-serde/BorshDecoder.sol around lines 14-106, the
decoder advances the Data cursor for each schema field but never asserts full
consumption, allowing extra trailing bytes to be ignored; add a final call to
data.done() (or equivalent check) after the for-loop and before returning to
revert on leftover bytes, and optimize the loop by computing bytes(typeName)
keccak256 once per iteration (store keccak256(bytes(typeName)) in a local
bytes32 variable and reuse it for all branch comparisons) to reduce repeated
hashing gas costs.
| function getCurrentPayloadId( | ||
| uint32 chainSlug_, | ||
| bytes32 switchboardType_ | ||
| ) public view returns (bytes32) { | ||
| uint32 switchboardId = switchboards[chainSlug_][switchboardType_]; | ||
| // Write payload: source = (evmxChainSlug, watcherId), verification = (dstChainSlug, dstSwitchboardId) | ||
| // watcherId hardcoded as 1 for now | ||
| return | ||
| createPayloadId( | ||
| evmxSlug, // source chain slug (evmx) | ||
| 1, // source id (watcher id, hardcoded) | ||
| chainSlug_, // verification chain slug (destination) | ||
| switchboardId, // verification id (destination switchboard) | ||
| nextPayloadCount // pointer (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.
Payload ID construction ties pointer to shared nextPayloadCount—ensure atomicity
getCurrentPayloadId builds the payloadId using nextPayloadCount:
return createPayloadId(
evmxSlug,
1,
chainSlug_,
switchboardId,
nextPayloadCount
);and executePayload then stores payloadPointer: nextPayloadCount++ in the Payload struct.
The math itself is consistent (ID pointer and stored pointer use the same pre‑increment value), but because nextPayloadCount is shared global state and staging/execution are split across addPayloadData and executePayload, any reordering or repeated calls can desynchronize:
- If
addPayloadDatais called twice beforeexecutePayload, both payloads will compute IDs using the samenextPayloadCountvalue, and the increment only happens once inexecutePayload. - That produces conflicting expectations about pointer→payload mapping.
This should ensure that ID creation and pointer increment are atomic with respect to each staged payload (e.g. increment nextPayloadCount at ID creation time and store the captured value in both the ID and the payload) and that the staging and execution flows cannot race.
Also applies to: 21-22
🤖 Prompt for AI Agents
In contracts/evmx/watcher/Watcher.sol around lines 328-343 (also apply similar
fix at lines 21-22): the payload ID uses the shared nextPayloadCount without
reserving/incrementing it atomically, which allows multiple staged payloads to
compute identical IDs; modify the flow so a pointer is reserved at ID creation
time — introduce a reservation step (e.g., reservePayloadPointer or change
getCurrentPayloadId to increment and return the captured pointer) that
increments nextPayloadCount and returns the reserved pointer, then use that
captured pointer when building the payloadId and when storing
payload.payloadPointer in executePayload (or pass the reserved pointer through
staging), ensuring the increment and ID construction are performed once
atomically for each staged payload.
| /// @notice Connects the plug to socket with app gateway and switchboard | ||
| /// @param appGatewayId_ The app gateway ID to associate with this plug | ||
| /// @param socket_ The socket contract address | ||
| /// @param switchboardId_ The switchboard ID to connect to | ||
| /// @dev Sets socket reference, stores appGatewayId, and calls socket.connect() | ||
| function _connectSocket( | ||
| bytes32 appGatewayId_, | ||
| address socket_, | ||
| uint64 switchboardId_ | ||
| uint32 switchboardId_ | ||
| ) internal { | ||
| _setSocket(socket_); | ||
| appGatewayId = 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.
Guard against zero-address socket in initialization
_setSocket accepts any address and initSocket does not validate socket_. A misconfigured deployment that passes address(0) will silently succeed, and every onlySocket check will revert forever.
This should validate socket_ != address(0) during initialization so misconfigurations fail fast:
function _setSocket(address socket_) internal {
- socket__ = ISocket(socket_);
+ if (socket_ == address(0)) revert NotSocket();
+ socket__ = ISocket(socket_);
}This keeps the error surface consistent with onlySocket and prevents bricked plugs due to bad deployment params.
Also applies to: 74-78, 95-101
🤖 Prompt for AI Agents
In contracts/protocol/base/PlugBase.sol around lines 50-61 (and similarly at
74-78, 95-101), the initializer path does not validate the provided socket
address which allows address(0) to be set and bricks the contract; add an
explicit require(socket_ != address(0), "Socket: zero address") (or the
project’s standard zero-address revert message used by onlySocket) at the start
of _connectSocket (and the other init locations) before calling _setSocket and
setting appGatewayId so misconfigured deployments fail fast and preserve
consistent error semantics.
| function _validateExecutionStatus(bytes32 payloadId_) internal { | ||
| if (payloadExecuted[payloadId_] == ExecutionStatus.Executed) | ||
| revert PayloadAlreadyExecuted(payloadExecuted[payloadId_]); | ||
| if (executionStatus[payloadId_] == ExecutionStatus.Executed) | ||
| revert PayloadAlreadyExecuted(); | ||
|
|
||
| payloadExecuted[payloadId_] = ExecutionStatus.Executed; | ||
| executionStatus[payloadId_] = ExecutionStatus.Executed; | ||
| } |
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.
Execution status allows re‑execution after revert, contradicting intent
_validateExecutionStatus only blocks when executionStatus[payloadId_] == ExecutionStatus.Executed and then unconditionally sets the status to Executed. _handleFailedExecution later overwrites the status to Reverted.
This means:
- First attempt: status goes 0 → Executed → Reverted (on failure).
- Second attempt: status is
Reverted, so_validateExecutionStatusdoes not revert and sets it back toExecuted, allowing re‑execution of a payload explicitly marked as reverted, despite the comment “Mark payload as reverted to prevent retry”.
This should treat any non‑zero status (Executed or Reverted) as terminal, or introduce an explicit “None/InProgress” state and block both Executed and Reverted from re‑entry.
For example:
function _validateExecutionStatus(bytes32 payloadId_) internal {
- if (executionStatus[payloadId_] == ExecutionStatus.Executed)
- revert PayloadAlreadyExecuted();
-
- executionStatus[payloadId_] = ExecutionStatus.Executed;
+ ExecutionStatus status = executionStatus[payloadId_];
+ if (status != ExecutionStatus(0)) revert PayloadAlreadyExecuted();
+ executionStatus[payloadId_] = ExecutionStatus.Executed;
}Adjust enum values as needed so ExecutionStatus(0) represents “not yet executed”.
Also applies to: 189-212
🤖 Prompt for AI Agents
In contracts/protocol/Socket.sol around lines 219-224 (and also apply same fix
to 189-212), the current `_validateExecutionStatus` only rejects when status ==
Executed and then unconditionally sets Executed, allowing re-entry when status
== Reverted; change the validation to treat any non-zero status as terminal by
adding/using an explicit zero state (e.g., ExecutionStatus.None = 0) and update
`_validateExecutionStatus` to revert unless executionStatus[payloadId_] ==
ExecutionStatus.None, then set it to Executed; ensure `_handleFailedExecution`
still writes Reverted on failure and that all enum usages assume 0 means “not
yet executed.”
| 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.
createSignature writes v outside the 65‑byte signature window
The assembly currently does:
sig = new bytes(65);
bytes1 v32 = bytes1(sigV);
assembly {
mstore(add(sig, 96), v32)
mstore(add(sig, 32), sigR)
mstore(add(sig, 64), sigS)
}mstore(add(sig, 96), v32) stores v in the last byte of a 32‑byte word starting at sig + 96, but the 65‑byte data region only includes the first byte at sig + 96. That byte becomes 0x00, so v ends up outside the logical bytes length and the returned signature is invalid.
This should write v as a single byte at offset 96 using mstore8, and it should store r/s before v:
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)
- }
+ bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_));
+ (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest);
+ sig = new bytes(65);
+ assembly {
+ mstore(add(sig, 32), sigR)
+ mstore(add(sig, 64), sigS)
+ mstore8(add(sig, 96), sigV)
+ }
}This should produce a standard r || s || v signature layout.
🤖 Prompt for AI Agents
In deprecated/test/protocol/Socket.t.sol around lines 138–151, the assembly
stores v outside the 65-byte bytes buffer using mstore(add(sig, 96), v32) which
places v in the high byte of a 32-byte word and outside the logical bytes
length; change the write order to store r and s at offsets add(sig,32) and
add(sig,64) (as already done) and write v as a single byte into offset
add(sig,96) using mstore8, ensuring the returned signature is in r || s || v
layout.
| import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol"; | ||
|
|
||
| // source .env && forge script script/helpers/DepositGasToken.s.sol --broadcast --skip-simulation | ||
| contract depositGasToken is Script { |
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
Fix naming inconsistency: depositGasToken vs filename.
The contract is named depositGasToken (lowercase 'd') but the filename is DepositGas.s.sol. This creates confusion. Use consistent PascalCase naming.
-contract depositGasToken is Script {
+contract DepositGas is Script {📝 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.
| contract depositGasToken is Script { | |
| contract DepositGas is Script { |
🤖 Prompt for AI Agents
In script/helpers/DepositGas.s.sol around line 10, the contract is declared as
"depositGasToken" (lowercase 'd') which mismatches the PascalCase filename;
rename the contract to "DepositGas" (or "DepositGasToken") to match the file
naming convention and update any references/imports/tests that use the old name
so the file and contract names are consistent and compile without confusion.
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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
contracts/protocol/switchboard/MessageSwitchboard.sol (1)
178-347: Honour the caller-specified deadline in the digestYou decode
deadlinein_decodeOverrides, even default it when unset, but_createDigestAndPayloadIdthrows that away and always hardcodesblock.timestamp + 3600. Payloads that should expire sooner (or later) end up with a one-hour window no matter what the caller requested. This should propagate the already-normalisedoverrides.deadlineinto digest construction instead of ignoring it.- ) = _createDigestAndPayloadId( + ) = _createDigestAndPayloadId( overrides.dstChainSlug, plug_, overrides.gasLimit, overrides.value, - payload_ + payload_, + overrides.deadline );- function _createDigestAndPayloadId( + function _createDigestAndPayloadId( uint32 dstChainSlug_, address plug_, uint256 gasLimit_, uint256 value_, - bytes calldata payload_ + bytes calldata payload_, + uint256 deadline_ ) internal returns ( DigestParams memory digestParams, bytes32 digest, bytes32 payloadId ) { @@ - digestParams = DigestParams({ + digestParams = DigestParams({ socket: siblingSockets[dstChainSlug_], transmitter: bytes32(0), payloadId: payloadId, - deadline: block.timestamp + 3600, + deadline: deadline_, callType: WRITE,
♻️ Duplicate comments (6)
contracts/protocol/SocketConfig.sol (2)
97-107: Validate non-zeronetworkFeeCollector_when used as a contract.The comment explicitly allows
address(0)to disable fee collection, but there is no validation when a non-zero address is configured. Misconfiguring this to an EOA or wrong contract will cause downstreamcollectNetworkFeecalls to revert. This should add a guard such asif (networkFeeCollector_ != address(0))and then ensure it is a contract (e.g.,extcodesize > 0) before assigning, to fail fast on bad governance input.
178-187: GuardgetPlugConfigagainst disconnected plugs.
getPlugConfigreadsswitchboardId = plugSwitchboardIds[plugAddress_]and immediately callsISwitchboard(switchboardAddresses[switchboardId]). WhenswitchboardId == 0(disconnected), this calls intoaddress(0)and yields a generic revert instead of the domain-specificPlugNotConnected(). This should explicitlyrevert PlugNotConnected()whenswitchboardId == 0before dereferencingswitchboardAddresses.contracts/utils/common/Structs.sol (1)
146-150:RawPayloadis minimal and pushes routing data intoOverrideParams.Having
RawPayloadholdOverrideParams, aTransaction, andasyncPromisekeeps the core payload representation slim. This reflects the move away from embedding extra fields (like switchboardType) directly on RawPayload, which is consistent with updated callers.test/PausableTest.t.sol (1)
224-244: Fix or remove watcher revert tests that never call the target functions.
test_Watcher_ExecutePayload_WhenPaused_ShouldRevertandtest_Watcher_ResolvePayload_WhenPaused_ShouldRevertonly pause the watcher and assertwatcher.paused()without callingexecutePayload/resolvePayload, despite their names. This should either:
- Call the respective functions under
vm.expectRevert(Pausable.ContractPaused.selector), or- Be removed, since the lower-case variants below already test the revert behavior properly.
contracts/protocol/Socket.sol (1)
215-220: Block replays after marking execution as reverted
_handleFailedExecutionstoresExecutionStatus.Revertedwith the explicit intent to “prevent retry”, yet_validateExecutionStatusonly rejects theExecutedstate. Any caller can resubmit the samepayloadId,Revertedsilently flips back toExecuted, and the target call runs again. That defeats the single-execution guarantee and re-opens the attack surface you just tried to close. This should treat every non-default status as terminal before marking the payload as executed.- if (executionStatus[payloadId_] == ExecutionStatus.Executed) + ExecutionStatus status = executionStatus[payloadId_]; + if (status != ExecutionStatus.NotExecuted) revert PayloadAlreadyExecuted(); - - executionStatus[payloadId_] = ExecutionStatus.Executed; + executionStatus[payloadId_] = ExecutionStatus.Executed;contracts/evmx/watcher/Watcher.sol (1)
64-79: Lock down payload staging to authorized callers
addPayloadDatais wide open. Any address can call it, choose arbitraryappGateway_, and write attacker-controlledpayloadData. Worse, by settingrawPayload_.asyncPromiseto a non-zero sentinel they satisfy the guard once and leavepayloadData.asyncPromise != address(0), permanently reverting every subsequent legitimate attempt. That is a straightforward DoS against the watcher pipeline. This should only accept calls from a trusted actor (e.g., the watcher role) instead of an unchecked external account, so a random user cannot freeze execution.- function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) external { + function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) + external + onlyWatcher + { if (payloadData.asyncPromise != address(0)) revert PayloadAlreadySet();
🧹 Nitpick comments (7)
contracts/protocol/SocketConfig.sol (3)
25-51: ConstrainmaxCopyBytesand align withMAX_COPY_BYTES.
maxCopyBytesis hard-coded to 2048 whileMAX_COPY_BYTESis imported but unused, andsetMaxCopyByteshas no bounds. This should either initialize fromMAX_COPY_BYTESor validatemaxCopyBytes_against it (and> 0) so governance cannot accidentally set an unsafe or useless value (e.g., 0 or an extremely large number).
60-73: GuardregisterSwitchboard/ID counter for invalid or overflow states.
switchboardIdCounterisuint32and unbounded;registerSwitchboarddoes not guard against overflow or invalidmsg.sender. While overflow is unlikely, this should at leastrevertifswitchboardIdCounter == type(uint32).maxbefore increment so the system fails fast instead of silently wrapping to 0 (which is treated as “not registered” elsewhere).
155-169: Consider validatingmaxCopyBytes_insetMaxCopyBytes.
setMaxCopyBytesaccepts anyuint16, including 0, which would break simulations, and values aboveMAX_COPY_BYTESdespite importing that constant. This should enforce a safe range, e.g.if (maxCopyBytes_ == 0 || maxCopyBytes_ > MAX_COPY_BYTES) revert InvalidMaxCopyBytes();, or at least document clearly that 0 is intentional.contracts/protocol/NetworkFeeCollector.sol (1)
63-70:collectNetworkFeeonly validates and records payment; funds stay in contract.
collectNetworkFeeenforcesmsg.value >= networkFeeand emitsNetworkFeeCollectedbut does not forward or refund any portion. This is acceptable if the design is to accumulate fees in this contract and letRESCUE_ROLE/governance handle withdrawals viarescueFunds, but the intended lifecycle of collected fees should be clearly documented for operators so they do not assume automatic forwarding.contracts/protocol/switchboard/EVMxSwitchboard.sol (3)
70-79: Constructor wires EVMX identifiers but does not validate them.
evmxChainSlug_andwatcherId_are accepted as-is and only later checked inprocessPayload(EvmxConfigNotSetif either is zero). This is workable if the config might legitimately be “unset” at deployment, but it should be intentional; if unset configs are never valid, this constructor should revert on zero to fail early.
139-171:processPayloadbuilds a well-scopedpayloadIdbut should ensure overrides encoding consistency.Building
payloadIdfrom(chainSlug, switchboardId, evmxChainSlug, watcherId, payloadCounter++)is consistent with the payloadId scheme used elsewhere and storespayloadIdToPlug[payloadId]for later fee operations. The override handling assumesoverrides_is either empty or a singleuint256deadline; this should stay consistent with any higher-level encoding format, otherwise decoding here will break when more override fields are added.
197-201:updatePlugConfigassumesplugConfig_is exactly abytes32app gateway ID.Decoding
plugConfig_as(bytes32)and writing directly intoplugAppGatewayIdsis fine as long as all callers respect this encoding. If you plan to extend plug config in future, this function should either version or validate the length ofplugConfig_to avoid mis-decodes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
foundry.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
contracts/evmx/watcher/Watcher.sol(3 hunks)contracts/protocol/NetworkFeeCollector.sol(1 hunks)contracts/protocol/Socket.sol(1 hunks)contracts/protocol/SocketConfig.sol(1 hunks)contracts/protocol/SocketUtils.sol(3 hunks)contracts/protocol/switchboard/EVMxSwitchboard.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(5 hunks)contracts/protocol/switchboard/SwitchboardBase.sol(2 hunks)contracts/utils/common/AccessRoles.sol(2 hunks)contracts/utils/common/Structs.sol(7 hunks)test/PausableTest.t.sol(1 hunks)
🔇 Additional comments (34)
contracts/protocol/SocketConfig.sol (1)
195-200:getPlugSwitchboardbehavior is consistent with docs.Returning
(0, address(0))for unconnected plugs matches the documented behavior and works well with callers that explicitly interpret 0 as “not connected”.contracts/utils/common/AccessRoles.sol (1)
15-22: New role constants are coherent with access pattern.The added
GAS_MANAGER_ROLE,FEE_UPDATER_ROLE,PAUSER_ROLE, andSOCKET_ROLEline up with the broader role-based access model used elsewhere in the PR and keep responsibilities separated, which will simplify audits around who can pause, manage gas, or act as socket.test/PausableTest.t.sol (3)
55-152: Socket pausing tests correctly assert role gates and revert behavior.The Socket tests exercise: owner not being able to pause,
PAUSER_ROLEgating pause,GOVERNANCE_ROLEgating unpause, andexecutereverting withPausable.ContractPaused. This matches the intended semantics and gives good coverage for the pause path.
176-222: Watcher pause/unpause tests are aligned with role model.Watcher tests for pauser and unpauser roles correctly assert that only
PAUSER_ROLEcan pause and only aGOVERNANCE_ROLEgrantee can unpause, with unauthorized callers reverting viaAccessControl.NoPermit. This matches the Socket behavior and gives a consistent pause model across components.
246-273: Lower-case watcher revert tests correctly assert pause behavior.
test_Watcher_executePayload_WhenPaused_ShouldRevertandtest_Watcher_resolvePayload_WhenPaused_ShouldRevertnow correctly callexecutePayload/resolvePayloadundervm.expectRevert(Pausable.ContractPaused.selector). These are the tests that actually validate the modifiers and should remain even if the older uppercase variants are removed.contracts/protocol/NetworkFeeCollector.sol (3)
45-52: Constructor wiring for roles and initial fee looks correct.Assigning
GOVERNANCE_ROLEandRESCUE_ROLEtoowner_andSOCKET_ROLEtosocket_, plus emittingNetworkFeeUpdated(0, networkFee_), cleanly initializes the contract and makes fee configuration auditable.
86-89:setNetworkFeecorrectly emits the delta and enforces governance control.Restricting
setNetworkFeetoGOVERNANCE_ROLEand emitting the old and new values ensures a clear on-chain audit trail of fee changes.
100-106: Rescue path is consistent with sharedRescueFundsLibpattern.
rescueFundsis gated byRESCUE_ROLEand delegates toRescueFundsLib._rescueFunds, which centralizes the fund recovery logic and avoids duplicating transfer code across contracts.contracts/protocol/switchboard/SwitchboardBase.sol (5)
32-36:onlySocketmodifier correctly enforces caller restrictions with a precise error.Restricting certain functions to
msg.sender == address(socket__)and reverting withNotSocket()gives a clear, domain-specific failure instead of a generic or silent revert.
60-71:getTransmitterdigest construction is straightforward and side-effect free.Using
keccak256(abi.encodePacked(address(socket__), payloadId_))plus_recoverSignerkeeps the digest small and deterministic. Returningaddress(0)when no signature is provided makes it easy for callers to distinguish “no signer” from “invalid signer”.
81-88:_recoverSigneruses the standard Ethereum signed-message prefix.Wrapping the digest with
\x19Ethereum Signed Message:\n32before ECDSA recovery matches typicaleth_signsemantics and is documented as such, which reduces confusion for off-chain signers.
99-105: Rescue implementation matches the shared pattern.
rescueFundsis restricted toRESCUE_ROLEand routes throughRescueFundsLib._rescueFunds, which centralizes transfer logic and simplifies audits.
40-48: Constructor and switchboard registration are correctly implemented.The constructor properly validates inputs and registers with the socket during initialization. The call to
socket__.registerSwitchboard()executes with the switchboard's own address asmsg.sender, which correctly maps to the SocketConfig registry. The tight coupling you noted is real—if Socket's registration logic changes (e.g., access control, ID assignment strategy), derived switchboards must stay synchronized or deployment will fail. But the current implementation has no issues.contracts/protocol/SocketUtils.sol (8)
34-40: Constructor enforces non-zerochainSlugandownerand sets immutable chain identity.Validating
chainSlug_andowner_before assignment ensures this Socket instance is always bound to a valid chain and owner; using an immutablechainSlugmakes payload verification logic in_verifyPayloadIdrobust.
53-83: Digest construction is collision-resistant and covers all relevant fields.
_createDigestencodes all fixed-size fields first, then appends variable-lengthpayload,source, andextraDatawith explicituint32length prefixes. This should prevent ambiguous encodings and ensures that any change to payload or meta-data produces a different digest.
92-103: Simulation helper correctly bounds return data usingmaxCopyBytes.
simulateloops overSimulateParams, callingtryCallwithmaxCopyBytesand returning(success, returnData, exceededMaxCopy)per entry. This keeps off-chain gas estimation safe from unbounded return data while exposing enough detail for analysis.
114-122:_verifyPlugSwitchboardenforces both connection and switchboard status.Checking
plugSwitchboardIds[plug_] != 0andswitchboardStatus[switchboardId] == SwitchboardStatus.REGISTEREDbefore returning the switchboard address gives a single, well-defined gate that all plug flows (e.g., fee increases) can reuse.
130-139:_verifyPayloadIdlinks payload IDs back to chain and switchboard.Decoding verification chain slug and switchboard ID from
payloadId_and comparing againstchainSlugandswitchboardAddresses[verificationSwitchboardId]ensures that only payloads destined for this chain and switchboard can be processed, which is critical for cross-chain safety.
151-160:increaseFeesForPayloadcorrectly reuses_verifyPlugSwitchboardand forwards value.By verifying the caller plug first and then forwarding
msg.valueplusfeesData_to the switchboard, this keeps all validation centralized and avoids duplicated access checks in switchboard implementations.
171-177: Rescue logic is consistent with other modules.
rescueFundsis guarded byRESCUE_ROLEand delegates toRescueFundsLib._rescueFunds, matching the pattern in other protocol contracts for safely recovering stuck funds.
181-191: Pause/unpause role assignments are consistent with tests.Gating
pause()behindPAUSER_ROLEandunpause()behindGOVERNANCE_ROLEmatches the behavior asserted inPausableTest.t.soland centralizes control of pausing at the governance layer.contracts/utils/common/Structs.sol (9)
32-43: Escrow types look suitable for tracking per-account gas/fee balances.
EscrowStateplusEscrowEntry {account, amount, state}provide clear state transitions (None → Active → Released/Settled) and use types (address,uint256) that fit the expected accounting ranges.
58-66: NarrowingswitchboardIdtouint32aligns with other components.Both
PlugConfigGenericandPlugConfigEvmnow useuint32 switchboardId, matchingSocketConfigandSwitchboardBase. This reduces storage and keeps ID types consistent across the protocol.
83-94:ExecutionParamsnow contains all fields required for digesting and execution.Including
target,payloadId, andsourcedirectly inExecutionParamssimplifies downstream code (e.g., digest creation, fee collection) and centralizes the execution contract, payload identity, and origin metadata in one struct.
110-124:DigestParamsmirrorsExecutionParamswith hashing-friendly types.Representing addresses as
bytes32(for socket, transmitter, target) and packingpayload,source, andextraDataas raw bytes gives a consistent format for building and verifying digests across components.
127-138: Override and fee-related fields cover key execution modifiers.
OverrideParams(isParallelCall, consumeFrom, switchboardType, maxFees) and the updatedvalue/writeFinalitylayout give sufficient flexibility to override concurrency, funding source, switchboard routing, and maximum fees without needing ad-hoc parameters elsewhere.
152-167:Payloadtracks execution, cancellation, and fee state comprehensively.The added flags (
isPayloadCancelled,isPayloadExecuted,isTransmitterFeesSettled) pluswatcherFeesandmaxFeesgive an explicit on-chain view of lifecycle and fee caps, which is important for auditing and safe refunds.
225-238:PayloadFeesandSponsoredPayloadFeesseparate native and sponsored flows.Splitting native-fee accounting (
PayloadFees) from sponsorship caps (SponsoredPayloadFees) with explicit refund fields (isRefundEligible,isRefunded,refundAddress) should make it easier to keep refund logic correct and auditable.
243-252:MessageOverridesconsolidates override decoding into a single struct.Capturing sponsorship flag, destination slug, gas limit, deadlines, refund/sponsor addresses, value, and maxFees in
MessageOverridesis a clean internal representation for precompiles and watcher logic to derive behavior from encoded overrides.
255-267: Simulation structs align withSocketUtils.simulate.
SimulateParamsandSimulationResultmatch the usage inSocketUtils.simulate, making it easy to evolve the simulation interface without touching unrelated code.contracts/protocol/switchboard/EVMxSwitchboard.sol (4)
90-105:attestcorrectly prevents duplicates and enforcesWATCHER_ROLE.The function ensures a digest cannot be attested twice, recovers the watcher from a digest including
address(this)andchainSlug, and then checks_hasRole(WATCHER_ROLE, watcher)before marking it as attested. This is the right pattern to avoid rogue attestations.
116-127:allowPayloadtightly couples app gateway ID to plug registration.Decoding
bytes32fromsource_and comparing it withplugAppGatewayIds[target_]before checkingisAttested[digest_]ensures that only payloads whose source matches the registered app gateway for a plug are allowed. This is an important guard against routing a digest to the wrong plug.
219-222:setDefaultDeadlineusesonlyOwnerand correctly emits updates.Restricting default deadline changes to the owner and emitting
DefaultDeadlineSetkeeps deadline policy tightly controlled and auditable.
230-235:getPlugConfigreturns a simple encoded app gateway ID, matchingupdatePlugConfig.Encoding
plugAppGatewayIds[plug_]back into bytes keepsgetPlugConfigsymmetric withupdatePlugConfigand makes it easy for configuration tooling to decode a singlebytes32app gateway ID.
| function disableSwitchboard( | ||
| uint64 switchboardId_ | ||
| uint32 switchboardId_ | ||
| ) external onlyRole(SWITCHBOARD_DISABLER_ROLE) { | ||
| isValidSwitchboard[switchboardId_] = SwitchboardStatus.DISABLED; | ||
| switchboardStatus[switchboardId_] = SwitchboardStatus.DISABLED; | ||
| emit SwitchboardDisabled(switchboardId_); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Enables a switchboard if disabled | ||
| * @dev This function is called by the governance role to enable a switchboard | ||
| * @param switchboardId_ The id of the switchboard to enable | ||
| * @notice Re-enables a previously disabled switchboard | ||
| * @param switchboardId_ The ID of the switchboard to enable | ||
| * @dev Only callable by GOVERNANCE_ROLE. Sets status back to REGISTERED. | ||
| */ | ||
| function enableSwitchboard(uint64 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { | ||
| isValidSwitchboard[switchboardId_] = SwitchboardStatus.REGISTERED; | ||
| function enableSwitchboard(uint32 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { | ||
| switchboardStatus[switchboardId_] = SwitchboardStatus.REGISTERED; | ||
| emit SwitchboardEnabled(switchboardId_); | ||
| } |
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.
Validate switchboard existence in disableSwitchboard / enableSwitchboard.
Both disableSwitchboard and enableSwitchboard blindly update switchboardStatus without ensuring switchboardAddresses[switchboardId_] != address(0). Enabling or disabling an ID that was never registered will mark it as REGISTERED/DISABLED but still point to address(0), which later causes generic “call to non-contract” errors when plugs interact. This should revert with InvalidSwitchboard() when switchboardAddresses[switchboardId_] == address(0).
🤖 Prompt for AI Agents
In contracts/protocol/SocketConfig.sol around lines 80 to 95, both
disableSwitchboard and enableSwitchboard update switchboardStatus without
checking that the switchboard exists; add a guard at the start of each function
that requires switchboardAddresses[switchboardId_] != address(0) and revert with
InvalidSwitchboard() when it is address(0). Apply this check to both functions
before modifying status or emitting events so attempts to enable/disable
unregistered IDs revert immediately.
| function connect(uint32 switchboardId_, bytes memory plugConfig_) external override { | ||
| // Validate switchboard exists and is registered | ||
| if ( | ||
| switchboardId_ == 0 || switchboardStatus[switchboardId_] != SwitchboardStatus.REGISTERED | ||
| ) revert InvalidSwitchboard(); | ||
|
|
||
| // Store plug-to-switchboard mapping | ||
| plugSwitchboardIds[msg.sender] = switchboardId_; | ||
|
|
||
| // Forward config to switchboard if provided | ||
| if (plugConfig_.length > 0) { | ||
| ISwitchboard(switchboardAddresses[switchboardId_]).updatePlugConfig( | ||
| msg.sender, | ||
| plugConfig_ | ||
| ); | ||
| } | ||
| emit PlugConnected(msg.sender, switchboardId_, plugConfig_); | ||
| } |
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.
Also validate switchboardAddresses[switchboardId_] in connect.
connect checks switchboardStatus[switchboardId_] == SwitchboardStatus.REGISTERED but does not verify that switchboardAddresses[switchboardId_] != address(0). If governance accidentally calls enableSwitchboard on an unregistered ID, plugs will be allowed to connect but updatePlugConfig will be invoked on address(0) and revert. This should additionally require switchboardAddresses[switchboardId_] != address(0) (and reuse a single local variable for the address).
🤖 Prompt for AI Agents
In contracts/protocol/SocketConfig.sol around lines 117 to 134, the connect
function validates switchboardStatus but does not verify
switchboardAddresses[switchboardId_] is non-zero and currently calls
ISwitchboard(... ) inline; change it to read
switchboardAddresses[switchboardId_] into a local address variable, require that
it != address(0) (alongside the existing status check), use that local variable
for both storing/validating and for the ISwitchboard call to updatePlugConfig to
avoid calling address(0) and reduce redundant storage accesses.
| function increaseFeesForPayload( | ||
| bytes32 payloadId_, | ||
| address plug_, | ||
| bytes calldata feesData_ | ||
| ) external override payable onlySocket { | ||
| if (payloadIdToPlug[payloadId_] != plug_) revert InvalidSource(); | ||
| emit FeesIncreased(payloadId_, plug_, feesData_); | ||
| } |
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.
increaseFeesForPayload validates plug–payload binding but is currently a no-op on-chain.
The function ensures payloadIdToPlug[payloadId_] == plug_ before emitting FeesIncreased but does not actually move or account for additional fees. This matches the comment (“we don't support increasing fees... yet”), but callers must not assume any on-chain effect beyond the event.
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: 2
♻️ Duplicate comments (9)
contracts/protocol/switchboard/EVMxSwitchboard.sol (2)
167-182:increaseFeesForPayloadaccepts ETH but only emits an eventThis function validates the plug–payload binding and emits FeesIncreased but does not record or route msg.value anywhere on-chain. Callers that send ETH here will just increase this contract’s balance without affecting execution or refunds. This should either:
- Reject nonzero msg.value until on‑chain fee accounting is implemented, or
- Persist and integrate the additional fees into whatever off‑chain/on‑chain accounting will eventually be used.
197-206: Access control onsetRevertingPayloaddoes not match NatSpecThe NatSpec says “Only callable by owner” but the function uses onlyRole(WATCHER_ROLE). This should either:
- Switch to onlyOwner / an owner‑equivalent governance role, or
- Update the documentation to clearly state that WATCHER_ROLE accounts can mark payloads as reverting.
Keeping docs and behavior aligned avoids incorrect operational assumptions.contracts/protocol/SocketConfig.sol (3)
78-93: Validate switchboard existence indisableSwitchboard/enableSwitchboarddisableSwitchboard and enableSwitchboard update switchboardStatus without checking that a switchboard was actually registered for switchboardId_. This should:
- Require switchboardAddresses[switchboardId_] != address(0) before changing status, and
- Revert with InvalidSwitchboard() otherwise.
Without this, governance can accidentally mark an unregistered ID as REGISTERED, and connect() will accept it but later external calls will revert when interacting with address(0).
function disableSwitchboard( uint32 switchboardId_ ) external onlyRole(SWITCHBOARD_DISABLER_ROLE) { - switchboardStatus[switchboardId_] = SwitchboardStatus.DISABLED; + address switchboard = switchboardAddresses[switchboardId_]; + if (switchboard == address(0)) revert InvalidSwitchboard(); + switchboardStatus[switchboardId_] = SwitchboardStatus.DISABLED; emit SwitchboardDisabled(switchboardId_); } function enableSwitchboard(uint32 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { - switchboardStatus[switchboardId_] = SwitchboardStatus.REGISTERED; + address switchboard = switchboardAddresses[switchboardId_]; + if (switchboard == address(0)) revert InvalidSwitchboard(); + switchboardStatus[switchboardId_] = SwitchboardStatus.REGISTERED; emit SwitchboardEnabled(switchboardId_); }
115-128: Guardconnectagainst switchboards with zero addressconnect() checks that status is REGISTERED but does not verify switchboardAddresses[switchboardId_] is nonzero. After an erroneous enableSwitchboard on an unregistered ID, this allows plugs to “connect” to an ID whose address is address(0), and subsequent interactions will revert on calls to a non‑contract. This should require a nonzero address and reuse it as a local variable for the external call.
function connect(uint32 switchboardId_, bytes memory plugConfig_) external override { if ( switchboardId_ == 0 || switchboardStatus[switchboardId_] != SwitchboardStatus.REGISTERED ) revert InvalidSwitchboard(); - plugSwitchboardIds[msg.sender] = switchboardId_; - if (plugConfig_.length > 0) { - ISwitchboard(switchboardAddresses[switchboardId_]).updatePlugConfig( + address switchboard = switchboardAddresses[switchboardId_]; + if (switchboard == address(0)) revert InvalidSwitchboard(); + + plugSwitchboardIds[msg.sender] = switchboardId_; + if (plugConfig_.length > 0) { + ISwitchboard(switchboard).updatePlugConfig( msg.sender, plugConfig_ ); } emit PlugConnected(msg.sender, switchboardId_, plugConfig_); }
172-181:getPlugConfigshould revert cleanly for disconnected plugsgetPlugConfig reads switchboardId = plugSwitchboardIds[plugAddress_] and immediately calls ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig. When switchboardId is zero (disconnected plug), this calls into address(0) and yields a generic “call to non‑contract” instead of the intended PlugNotConnected() error. This should detect switchboardId == 0 and revert explicitly before attempting the external call.
function getPlugConfig( address plugAddress_, bytes memory extraData_ ) external view returns (uint32 switchboardId, bytes memory plugConfig) { switchboardId = plugSwitchboardIds[plugAddress_]; + if (switchboardId == 0) revert PlugNotConnected(); - plugConfig = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig( + address switchboard = switchboardAddresses[switchboardId]; + plugConfig = ISwitchboard(switchboard).getPlugConfig( plugAddress_, extraData_ ); }contracts/protocol/switchboard/MessageSwitchboard.sol (4)
138-149: Validate sibling config inputs, includingswitchboardId_setSiblingConfig stores socket_, switchboard_, and switchboardId_ for a chainSlug_ without any validation. If any of these are zero, later calls (e.g., _validateSibling or _createDigestAndPayloadId) will see inconsistent config and revert with SiblingSocketNotFound or allow invalid IDs to leak into payload IDs. This should reject zero values up front.
function setSiblingConfig( uint32 chainSlug_, bytes32 socket_, bytes32 switchboard_, uint32 switchboardId_ ) external onlyOwner { + if (socket_ == bytes32(0) || switchboard_ == bytes32(0) || switchboardId_ == 0) { + revert SiblingSocketNotFound(); + } siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }
151-157: Access control onsetRevertingPayloadconflicts with commentThe comment says “Only callable by owner” but the function is restricted by onlyRole(WATCHER_ROLE). This should either:
- Use onlyOwner (or an equivalent governance role), or
- Update the NatSpec to state clearly that WATCHER_ROLE accounts are allowed to mark payloads as reverting.
168-239: Overrides deadline is decoded but not used when building digests_processPayload decodes overrides via _decodeOverrides, which computes a concrete deadline (falling back to defaultDeadline when 0). However, _createDigestAndPayloadId ignores that field and hardcodes digestParams.deadline = block.timestamp + 3600. This means:
- Off‑chain components reading MessageOutbound.digestParams will see a deadline unrelated to the one encoded in overrides, and
- Changing defaultDeadline or per‑message deadlines has no effect on the actual digest/expiry logic.
This should either:
- Use overrides.deadline when populating digestParams.deadline, with a sanity check that it is > block.timestamp, or
- Remove deadline from MessageOverrides altogether if per‑message deadlines are not supported.
function _createDigestAndPayloadId( uint32 dstChainSlug_, address plug_, uint256 gasLimit_, uint256 value_, bytes calldata payload_ ) internal returns (DigestParams memory digestParams, bytes32 digest, bytes32 payloadId) { ... - digestParams = DigestParams({ + digestParams = DigestParams({ socket: siblingSockets[dstChainSlug_], transmitter: bytes32(0), payloadId: payloadId, - deadline: block.timestamp + 3600, + // Use the override deadline computed in _decodeOverrides + deadline: overrides.deadline, callType: WRITE, gasLimit: gasLimit_, value: value_, payload: payload_, target: siblingPlugs[dstChainSlug_][plug_], source: abi.encodePacked(chainSlug, toBytes32Format(plug_)), prevBatchDigestHash: bytes32(0), extraData: bytes("") });You will need to thread the overrides struct into _createDigestAndPayloadId to apply this cleanly.
709-723: Plug config update should reject zero sibling plug entriesupdatePlugConfig decodes (siblingChainSlug, siblingPlug), ensures siblingSockets and siblingSwitchboards are configured, and then sets siblingPlugs[siblingChainSlug][plug_] = siblingPlug. If siblingPlug is bytes32(0), subsequent _validateSibling and allowPayload calls will treat this plug as misconfigured. This should explicitly reject siblingPlug == bytes32(0) to avoid silently installing unusable configs.
function updatePlugConfig( address plug_, bytes memory plugConfig_ ) external override onlySocket { (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); if ( siblingSockets[siblingChainSlug] == bytes32(0) || siblingSwitchboards[siblingChainSlug] == bytes32(0) ) { revert SiblingSocketNotFound(); } + if (siblingPlug == bytes32(0)) { + revert SiblingSocketNotFound(); + } siblingPlugs[siblingChainSlug][plug_] = siblingPlug; emit PlugConfigUpdated(plug_, siblingChainSlug, siblingPlug); }
🧹 Nitpick comments (5)
contracts/protocol/switchboard/EVMxSwitchboard.sol (1)
70-79: Consider emittingEvmxConfigSetwhen constructor sets configevmxChainSlug and watcherId are initialized in the constructor but EvmxConfigSet is never emitted. If off‑chain systems rely on that event to detect configuration, this should emit once in the constructor (or in a dedicated setter) using the stored values.
contracts/protocol/SocketConfig.sol (1)
100-105: Network fee collector setter lacks interface/contract validationsetNetworkFeeCollector now explicitly allows address(0) to disable fee collection, which is fine. For nonzero addresses, this should still validate that the target is a contract and (ideally) implements INetworkFeeCollector to avoid misconfiguration that silently breaks fee collection.
contracts/protocol/Socket.sol (1)
214-219: Execution status allows retries after explicit revertsvalidateExecutionStatus only blocks when executionStatus[payloadId] == ExecutionStatus.Executed and sets the status to Executed up front. _handleFailedExecution then overwrites the status to Reverted. This means reverted payloads remain retryable, while successfully executed ones are terminal. If the intended behavior is to treat Reverted as a terminal state too, this should instead block on any non‑zero status and reserve 0 for “never tried”.
function _validateExecutionStatus(bytes32 payloadId_) internal { - if (executionStatus[payloadId_] == ExecutionStatus.Executed) - revert PayloadAlreadyExecuted(); - - executionStatus[payloadId_] = ExecutionStatus.Executed; + ExecutionStatus status = executionStatus[payloadId_]; + if (status != ExecutionStatus(0)) revert PayloadAlreadyExecuted(); + executionStatus[payloadId_] = ExecutionStatus.Executed; }If retries after revert are intentional, document that explicitly on the enum and this function.
contracts/protocol/switchboard/MessageSwitchboard.sol (2)
596-612: Native fee top‑up validates ownership but ignoresfeesData_contentsincreaseNativeFees ensures only the plug that created the payload can add native fees and increments nativeFees by msg.value. The feesData payload is logged but not parsed beyond feesType. If additional semantics (like per‑hop fee breakdown) are expected in feesData_, this should decode and validate them here; otherwise, the comment should explicitly state that feesData_ is observational only for now.
617-632: Sponsored fee updates trust caller plug and overwritemaxFeesincreaseSponsoredFees enforces that the calling plug matches the stored plug for the payload, then decodes and overwrites maxFees from feesData. This should be safe given the plug binding, but if the protocol only wants monotonic increases, this should also require newMaxFees >= fees.maxFees to prevent accidental fee caps from being lowered.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
contracts/protocol/NetworkFeeCollector.sol(1 hunks)contracts/protocol/Socket.sol(1 hunks)contracts/protocol/SocketConfig.sol(1 hunks)contracts/protocol/SocketUtils.sol(3 hunks)contracts/protocol/switchboard/EVMxSwitchboard.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/protocol/NetworkFeeCollector.sol
🔇 Additional comments (18)
contracts/protocol/SocketUtils.sol (5)
53-83: Digest construction correctly defends against collision tricksFixed-size fields are packed first and all variable-length fields are length‑prefixed before hashing. This should give a stable, unambiguous digest layout for ExecutionParams.
92-106: Simulation helper is constrained and side‑effect free on-chainsimulate() restricts callers via onlyOffChain, bounds copied return data with maxCopyBytes, and never mutates state. This should keep it safe as an off‑chain gas/revert probe.
108-137: Plug/switchboard and payloadId verification are consistent with ID model_verifyPlugSwitchboard enforces a nonzero switchboardId and REGISTERED status, and _verifyPayloadId cross‑checks chainSlug and switchboard address from getVerificationInfo. This should prevent payloads verified for a different chain or switchboard from being executed here.
149-157: Fee top‑up flow cleanly delegates to switchboardincreaseFeesForPayload validates the caller plug via _verifyPlugSwitchboard and forwards both value and feesData to the switchboard. This should keep fee semantics centralized in switchboard implementations.
161-188: Rescue and pause wiring respects role separationRESCUE_ROLE is required for rescueFunds, PAUSER_ROLE for pause, and GOVERNANCE_ROLE for unpause. This should keep operational powers clearly separated from normal execution paths.
contracts/protocol/Socket.sol (4)
46-74: Execute path enforces core invariants before any untrusted callsexecute() checks deadline, WRITE callType, plug connectivity (via _verifyPlugSwitchboard), msg.value coverage (value + socketFees), payloadId verification, non‑terminal execution status, and switchboard attestation before calling into the plug. This should give a solid safety envelope around the external call.
123-157: Gas‑buffered execution with structured success/failure handling_execute() enforces a gasleft() threshold based on gasLimit * gasLimitBuffer, uses tryCall with maxCopyBytes, and routes to dedicated success/failure handlers before returning. This should avoid under‑gas edge cases and centralizes side‑effects (fees, refunds, events) away from the low‑level call site.
242-259: Outbound send path correctly enforces plug connectivity and isolates overrides_sendPayload() verifies the plug’s switchboard via _verifyPlugSwitchboard, fetches plug‑specific overrides from IPlug.overrides(), and delegates to ISwitchboard.processPayload with the supplied value and callData. This should keep outbound payload construction centralized and ensures only connected plugs can originate payloads.
268-271: Fallback wrapper returns double‑encoded payloadId as documentedThe fallback forwards to _sendPayload and returns abi.encode(abi.encode(payloadId)), matching the comment about manually wrapping the bytes32 into ABI‑encoded bytes for fallback semantics. This should be stable as long as callers follow the two‑step decode described in the comment.
contracts/protocol/switchboard/MessageSwitchboard.sol (9)
249-309: Override decoding covers both flows and default deadlines correctly_decodeOverrides cleanly distinguishes version 1 (native) and version 2 (sponsored), sets a default deadline when 0, and maps parameters into a single MessageOverrides struct. This should make it easy to evolve override formats while keeping downstream code simple.
430-449: Refund eligibility marking has proper authorization and replay protectionmarkRefundEligible checks for existing eligibility, ensures there are native fees to refund, binds the signature to (contract, chainSlug, payloadId, nonce), enforces WATCHER_ROLE on the signer, and tracks usedNonces per watcher. This should make watcher‑driven refunds robust against replay and spoofing.
455-466: Refund flow is state‑safe and reentrancy‑guardedrefund() asserts refund eligibility, prevents double refunds, zeroes nativeFees after latching isRefunded, and uses SafeTransferLib.safeTransferETH under nonReentrant. This should minimize refund‑related reentrancy and double‑spend risks.
475-539: Signature‑based min fee updates correctly protect against replayBoth setMinMsgValueFees and setMinMsgValueFeesBatch bind the digest to this contract, chainSlug, target chains/fees, and nonce, enforce FEE_UPDATER_ROLE on the signer, and store usedNonces[feeUpdater][nonce_]. This should give a solid oracle‑style update mechanism without replay across chains or payloads.
574-591: Fee type dispatch centralizes native vs sponsored handlingincreaseFeesForPayload decodes a leading feesType byte and routes to _increaseNativeFees or _increaseSponsoredFees, reverting on unknown types. This should keep the on‑chain ABI stable while allowing additional fee encodings to be added via new type values later.
641-655: Packed source decoding matchesabi.encodePacked(chainSlug, bytes32Plug)layout_decodePackedSource enforces a minimum length of 36 bytes and uses assembly to extract the right‑aligned uint32 and following bytes32. This matches the encoding in _createDigestAndPayloadId and should be compatible with non‑ABI environments like Solana.
660-669: allowPayload correctly binds digest to sibling plug topologyallowPayload decodes the (chainSlug, plug) tuple from sibling_, validates it against siblingPlugs[chainSlug][target_], and then checks that the digest has been attested. This should ensure only messages from the configured sibling plug and chain are accepted.
676-703: Digest creation mirrors Socket digest structure with length prefixes_createDigest builds a fixedPart of all scalar fields, then appends length‑prefixed payload, source, and extraData before hashing. This should align with SocketUtils._createDigest and minimize collision risk across variable‑length fields.
728-734: getPlugConfig exposes sibling plug per chain via encoded bytesgetPlugConfig decodes a chainSlug_ from extraData_ and returns abi.encode(siblingPlugs[chainSlug_][plug_]). This should be sufficient for off‑chain callers to recover the sibling plug value using a single abi.decode(bytes, (bytes32)).
| function _handleFailedExecution( | ||
| bytes32 payloadId_, | ||
| bool exceededMaxCopy_, | ||
| bytes memory returnData_, | ||
| address refundAddress_ | ||
| ) internal { | ||
| // Mark payload as reverted to prevent retry | ||
| executionStatus[payloadId_] = ExecutionStatus.Reverted; | ||
|
|
||
| // Refund msg.value to refundAddress or msg.sender if not specified | ||
| address receiver = refundAddress_; | ||
| if (receiver == address(0)) receiver = msg.sender; | ||
| SafeTransferLib.safeTransferETH(receiver, msg.value); | ||
|
|
||
| emit ExecutionFailed(payloadId_, exceededMaxCopy_, returnData_); | ||
| } |
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
SafeTransferLib is used but not imported in this file
_handleFailedExecution calls SafeTransferLib.safeTransferETH, but Socket.sol does not import SafeTransferLib. Unless another source file in the same compilation unit imports and exposes this symbol, this should cause an “identifier not found” compile error. This should explicitly import the library in this file.
-import "./SocketUtils.sol";
-import {WRITE} from "../utils/common/Constants.sol";
-import "../utils/common/Errors.sol";
-using LibCall for address;
+import "./SocketUtils.sol";
+import {WRITE} from "../utils/common/Constants.sol";
+import "../utils/common/Errors.sol";
+import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
+
+using LibCall for address;To confirm, compile the contracts or run a quick grep to ensure SafeTransferLib is imported somewhere reachable for this file.
🏁 Script executed:
#!/bin/bash
rg -n "SafeTransferLib" contractsLength of output: 1317
SafeTransferLib import is missing from Socket.sol
The grep confirms SafeTransferLib.safeTransferETH is called at line 204 in contracts/protocol/Socket.sol, but the library is not imported in that file. This will cause a compile error. Add the import:
import "./SocketUtils.sol";
import {WRITE} from "../utils/common/Constants.sol";
import "../utils/common/Errors.sol";
+import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In contracts/protocol/Socket.sol around lines 192 to 207, the file calls
SafeTransferLib.safeTransferETH but does not import SafeTransferLib; add the
proper import statement for SafeTransferLib at the top of the file (use the
project's standard path, e.g. solmate/utils/SafeTransferLib.sol or the local
utilities path) so the compiler can resolve SafeTransferLib and its
safeTransferETH function; no other code changes required.
contracts/protocol/Socket.sol
Outdated
| // Mark payload as reverted to prevent retry | ||
| executionStatus[payloadId_] = ExecutionStatus.Reverted; | ||
|
|
||
| // Refund msg.value to refundAddress or msg.sender if not specified | ||
| address receiver = refundAddress_; | ||
| if (receiver == address(0)) receiver = msg.sender; | ||
| SafeTransferLib.safeTransferETH(receiver, msg.value); | ||
|
|
||
| emit ExecutionFailed(payloadId_, exceededMaxCopy_, returnData_); | ||
| } |
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.
Failure refund uses full msg.value regardless of fee collector config
On failure, _handleFailedExecution refunds the entire msg.value to the refundAddress (or msg.sender). Combined with the earlier check that msg.value covers execution value plus socketFees, this is correct for the failure case. However, when networkFeeCollector is unset (address(0)), successful executions with nonzero socketFees will leave those fees trapped in this contract. This should either:
- Enforce socketFees == 0 when networkFeeCollector is unset, or
- Provide an explicit, governed drain path for accumulated fees.
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
♻️ Duplicate comments (10)
contracts/evmx/plugs/GasStation.sol (1)
13-20: Fix payloadId type inconsistency.The interface returns
bytes memorybut payloadId is consistentlybytes32throughout the codebase. This requires unnecessary encoding/decoding at lines 88-97. The interface should returnbytes32directly.Apply this diff:
interface IGasAccountManager { function depositFromChain( address token_, address receiver_, uint256 gasAmount_, uint256 nativeAmount_ - ) external returns (bytes memory); + ) external returns (bytes32); }contracts/protocol/switchboard/EVMxSwitchboard.sol (2)
163-170: Fee increase is intentionally a no-op.As documented in the comment at line 160, this function currently only emits an event for off-chain tracking. Fees are managed on EVMX. The validation ensures only the correct plug can trigger the event.
191-194: Documentation-implementation mismatch on access control.The comment states "Only callable by owner" but the implementation uses
onlyRole(WATCHER_ROLE). This should either useonlyOwneror update the documentation to reflect that watchers can mark payloads as reverting.contracts/protocol/SocketConfig.sol (3)
99-104: Consider validating non-zero addresses.While the documentation states address(0) is allowed to disable fee collection, validating that non-zero addresses are contracts implementing INetworkFeeCollector would prevent misconfiguration. This is optional given the explicit allowance of address(0).
77-92: Validate switchboard existence before status changes.Both disableSwitchboard and enableSwitchboard should verify
switchboardAddresses[switchboardId_] != address(0)before updating status. Otherwise, non-existent IDs can be marked as REGISTERED/DISABLED, causing confusing errors later.
114-127: Validate switchboard address exists in connect.The connect function should verify
switchboardAddresses[switchboardId_] != address(0)in addition to checking status. If governance accidentally enables an unregistered ID, plugs will connect but updatePlugConfig will fail on address(0).contracts/utils/common/Structs.sol (1)
146-150: Past review comment remains unaddressed.The RawPayload struct still does not directly expose switchboardType. Callers must access it via
rawPayload.overrideParams.switchboardType. The past review comment indicated that WritePrecompile Line 112 and ForwarderV2 Line 101 expectrawPayload.switchboardTypedirectly. This should either add switchboardType to RawPayload or update all callers.contracts/protocol/Socket.sol (1)
201-206: Re-execution after revert is allowed by current logic.The validation only blocks
ExecutionStatus.Executedbut notExecutionStatus.Reverted. This means a payload that failed and was marked as Reverted can be executed again (status would go Reverted → Executed on the second attempt). This contradicts the typical pattern where both Executed and Reverted should be terminal states.This should check for any non-zero status:
function _validateExecutionStatus(bytes32 payloadId_) internal { - if (executionStatus[payloadId_] == ExecutionStatus.Executed) + if (executionStatus[payloadId_] != ExecutionStatus.NotExecuted) revert PayloadAlreadyExecuted(); executionStatus[payloadId_] = ExecutionStatus.Executed; }contracts/protocol/switchboard/MessageSwitchboard.sol (2)
141-152: Validate sibling configuration parameters.The function should verify that
socket_,switchboard_, andswitchboardId_are non-zero before storing them. Zero values will cause SiblingSocketNotFound errors in processPayload.
656-670: Validate siblingPlug parameter is non-zero.The updatePlugConfig function should verify
siblingPlug_ != bytes32(0)after decoding. Allowing zero would cause _validateSibling to revert for subsequent payloads from that plug.
🧹 Nitpick comments (8)
auditor-docs/FAQ.md (2)
207-207: Fix compound adjective formatting.Line 207: "Worst Case Scenario" should be "Worst-case scenario" (hyphenated) when modifying a noun. Apply the hyphenation for proper grammar.
573-579: Reduce repetition of "Refunds only" throughout list.Lines 574-577 repeat "Refunds only" four times in a single list, making it verbose and unclear. Example:
- Line 574: "Refunds only to specified refundAddress"
- Line 576: "Refunds only when watcher-approved"
- Line 577: "Refunds only possible once"
Restructure to consolidate: "Refunds (only to specified refundAddress, only when watcher-approved, only once)" or separate into clearer bullet points with different phrasing.
auditor-docs/SETUP_GUIDE.md (4)
48-64: Add language identifier to code block for proper syntax highlighting.The project structure code block should specify a language identifier (e.g.,
textorbash) for proper rendering and syntax highlighting.Apply this diff:
-**Project Structure**: -``` -socket-protocol/ +**Project Structure**: +```text +socket-protocol/
243-248: Add language identifier to remappings code block.The remappings.txt example should specify a language identifier for consistent documentation formatting.
Apply this diff:
-### remappings.txt - -``` -solady/=lib/solady/src/ +### remappings.txt + +```text +solady/=lib/solady/src/
545-559: Add language identifier to contract addresses code blocks.The address examples should specify language identifiers for better documentation rendering.
Apply this diff:
**Sepolia**: -``` +```text Socket: 0x... (to be deployed)And:
**Arbitrum Sepolia**: -``` +```text Socket: 0x... (to be deployed)
578-584: Convert bare URLs to markdown links.Bare URLs should be converted to proper markdown links for better formatting and accessibility.
Apply this diff:
**Documentation**: -- Solidity Docs: https://docs.soliditylang.org/ -- Foundry Book: https://book.getfoundry.sh/ -- Solady Docs: https://github.com/Vectorized/solady +- [Solidity Docs](https://docs.soliditylang.org/) +- [Foundry Book](https://book.getfoundry.sh/) +- [Solady Docs](https://github.com/Vectorized/solady) **Security Resources**: -- Smart Contract Security Best Practices: https://consensys.github.io/smart-contract-best-practices/ -- DeFi Security Tools: https://github.com/crytic/building-secure-contracts +- [Smart Contract Security Best Practices](https://consensys.github.io/smart-contract-best-practices/) +- [DeFi Security Tools](https://github.com/crytic/building-secure-contracts)auditor-docs/README.md (1)
199-211: Consider adding version/priority information to the scope summary.The contract scope table includes LOC, complexity, and priority, which is excellent. However, consider adding a note about which Solidity version these contracts use (0.8.28 as mentioned elsewhere) directly in or near this table for quick reference.
auditor-docs/TESTING_COVERAGE.md (1)
13-22: Add language identifier to test structure code block.The test directory structure should specify a language identifier for proper rendering.
Apply this diff:
### Test Structure -``` +```text test/
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
auditor-docs/AUDIT_FOCUS_AREAS.md(1 hunks)auditor-docs/AUDIT_PREP_SUMMARY.md(1 hunks)auditor-docs/CONTRACTS_REFERENCE.md(1 hunks)auditor-docs/FAQ.md(1 hunks)auditor-docs/MESSAGE_FLOW.md(1 hunks)auditor-docs/README.md(1 hunks)auditor-docs/SECURITY_MODEL.md(1 hunks)auditor-docs/SETUP_GUIDE.md(1 hunks)auditor-docs/SYSTEM_OVERVIEW.md(1 hunks)auditor-docs/TESTING_COVERAGE.md(1 hunks)contracts/evmx/plugs/GasStation.sol(4 hunks)contracts/protocol/Socket.sol(1 hunks)contracts/protocol/SocketConfig.sol(1 hunks)contracts/protocol/SocketUtils.sol(3 hunks)contracts/protocol/switchboard/EVMxSwitchboard.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(3 hunks)contracts/protocol/switchboard/SwitchboardBase.sol(2 hunks)contracts/utils/common/Structs.sol(7 hunks)
🧰 Additional context used
🪛 LanguageTool
auditor-docs/FAQ.md
[uncategorized] ~207-~207: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... - Source validation in switchboards Worst Case Scenario: Governance could DOS the sy...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[style] ~485-~485: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...the source plug can increase fees - Can only increase, not decrease - Native fees: A...
(ADVERB_REPETITION_PREMIUM)
[style] ~575-~575: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...ource plug can increase fees 2. Refunds only to specified refundAddress 3. Refunds o...
(ADVERB_REPETITION_PREMIUM)
[style] ~576-~576: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...y to specified refundAddress 3. Refunds only when watcher-approved 4. Refunds only p...
(ADVERB_REPETITION_PREMIUM)
[style] ~577-~577: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...s only when watcher-approved 4. Refunds only possible once 5. Fees in successful exe...
(ADVERB_REPETITION_PREMIUM)
auditor-docs/SECURITY_MODEL.md
[style] ~128-~128: This phrase is redundant (‘OS’ stands for ‘operating system’). Use simply “DOS”.
Context: ...ior**: - May underpay fees - May try to DOS system with spam - May attempt double-spending...
(ACRONYM_TAUTOLOGY)
[grammar] ~327-~327: Use a hyphen to join words.
Context: ... 4. State Modification Points #### High Impact State Changes - `executionStatus[...
(QB_NEW_EN_HYPHEN)
[uncategorized] ~396-~396: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...n before connecting ### 7. No Built-in Rate Limiting - No on-chain rate limits for payload s...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~459-~459: Use a hyphen to join words.
Context: ...ds --- ## Threat Model Summary ### In Scope Threats - ✓ Malicious plugs attemp...
(QB_NEW_EN_HYPHEN)
auditor-docs/AUDIT_PREP_SUMMARY.md
[uncategorized] ~112-~112: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...g, not race condition; low probability, low impact - Verdict: Acceptable **9. Cross-C...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~116-~116: Ensure spelling is correct
Context: ...cy** - Reason: CEI pattern + unique payloadIds per call - Verdict: Safe by design ...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
auditor-docs/CONTRACTS_REFERENCE.md
[grammar] ~151-~151: Use a hyphen to join words.
Context: ...ess - increaseFeesForPayload(): Top up fees - Supports both native and spo...
(QB_NEW_EN_HYPHEN)
[grammar] ~206-~206: Use a hyphen to join words.
Context: ...mpler attestation model - App gateway ID based routing vs. sibling plug mapping ...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
auditor-docs/README.md
48-48: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
243-243: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
auditor-docs/FAQ.md
7-7: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
13-13: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
18-18: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
23-23: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
29-29: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
35-35: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
320-320: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
328-328: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
401-401: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
452-452: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
460-460: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
467-467: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
848-848: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
859-859: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
871-871: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
889-889: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
897-897: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
903-903: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
auditor-docs/MESSAGE_FLOW.md
13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
33-33: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
46-46: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
62-62: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
82-82: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
142-142: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
149-149: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
171-171: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
178-178: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
183-183: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
188-188: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
195-195: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
201-201: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
209-209: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
218-218: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
225-225: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
236-236: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
249-249: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
254-254: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
267-267: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
274-274: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
281-281: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
296-296: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
307-307: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
325-325: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
332-332: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
345-345: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
355-355: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
362-362: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
376-376: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
388-388: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
395-395: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
404-404: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
418-418: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
430-430: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
438-438: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
457-457: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
476-476: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
494-494: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
503-503: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
514-514: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
531-531: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
545-545: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
554-554: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
560-560: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
595-595: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
601-601: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
606-606: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
612-612: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
auditor-docs/SECURITY_MODEL.md
183-183: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
193-193: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
203-203: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
213-213: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
223-223: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
233-233: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
244-244: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
253-253: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
262-262: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
272-272: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
359-359: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
360-360: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
361-361: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
auditor-docs/SETUP_GUIDE.md
48-48: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
243-243: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
548-548: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
555-555: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
578-578: Bare URL used
(MD034, no-bare-urls)
579-579: Bare URL used
(MD034, no-bare-urls)
580-580: Bare URL used
(MD034, no-bare-urls)
583-583: Bare URL used
(MD034, no-bare-urls)
584-584: Bare URL used
(MD034, no-bare-urls)
auditor-docs/AUDIT_FOCUS_AREAS.md
29-29: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
34-34: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
39-39: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
44-44: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
83-83: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
87-87: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
91-91: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
95-95: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
99-99: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
103-103: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
107-107: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
111-111: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
115-115: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
119-119: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
182-182: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
187-187: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
193-193: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
198-198: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
203-203: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
230-230: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
235-235: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
240-240: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
245-245: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
250-250: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
auditor-docs/CONTRACTS_REFERENCE.md
16-16: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
auditor-docs/TESTING_COVERAGE.md
13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
173-173: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
189-189: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
204-204: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
220-220: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
230-230: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
240-240: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
250-250: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
262-262: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
271-271: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
280-280: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
295-295: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
304-304: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
315-315: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
324-324: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
338-338: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
348-348: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
358-358: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
371-371: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
383-383: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
394-394: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
408-408: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
419-419: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
430-430: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
443-443: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
453-453: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
466-466: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
476-476: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
486-486: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
498-498: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
508-508: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
522-522: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
533-533: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
545-545: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
555-555: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
564-564: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
575-575: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
585-585: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
595-595: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
610-610: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
621-621: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
632-632: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
643-643: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
654-654: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
680-680: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
700-700: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
714-714: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🔇 Additional comments (39)
auditor-docs/FAQ.md (1)
58-68: No action required—documentation reference is valid.The file
PAYLOAD_ID_ARCHITECTURE.mdexists in the repository. The reference at line 68 is correct and won't break the documentation chain.contracts/evmx/plugs/GasStation.sol (3)
62-63: Verify 10% native amount split is intentional.The function hardcodes a 90/10 split between gas and native amounts. This fixed ratio may not suit all use cases. Verify this business logic is correct.
111-112: IGasAccountToken properly includes bothbalanceOf(line 7) anddecimals(line 19) methods. The code is correct.
142-148: Type consistency verified—no issues found.The
uint32 switchboardId_parameter inGasStation.connectSocketis consistent withPlugBase._connectSocketand all other active implementations across the codebase. All non-deprecated code usesuint32, with only legacy code in thedeprecated/directory retaininguint64. The type change is properly coordinated throughout the entire system.auditor-docs/CONTRACTS_REFERENCE.md (1)
5-15: Add language identifier to table for proper markdown rendering.The contract inventory table code block should have a language identifier specified, though it's actually a markdown table (not a code block). This appears to be a false positive from the linter - the table is properly formatted with markdown syntax and doesn't need a code block.
auditor-docs/SYSTEM_OVERVIEW.md (1)
1-252: Excellent system overview documentation.This document provides a comprehensive overview of the Socket Protocol architecture, including clear articulation of the trust model, design decisions, and security properties. The ASCII diagrams and enumerated assumptions are particularly valuable for auditors.
auditor-docs/AUDIT_FOCUS_AREAS.md (1)
1-623: Comprehensive audit focus guide with actionable priorities.This document effectively prioritizes audit areas with specific line references, code examples, and security patterns. The multi-layer replay protection explanation and gas handling analysis are particularly thorough.
contracts/protocol/SocketUtils.sol (4)
34-40: Constructor validation is appropriate.The constructor now validates that
chainSlugis non-zero andowneris not the zero address before setting state. This prevents misconfiguration at deployment time.
54-84: Length-prefixed digest creation prevents collision attacks.The updated digest creation properly uses length prefixes for variable-length fields (payload, source, extraData). This prevents collision attacks where different input combinations could produce the same hash.
For example, without length prefixes:
payload="AB"+source="CD"would encode as"ABCD"payload="ABC"+source="D"would also encode as"ABCD"The length prefixes ensure these remain distinct.
178-185: Pause/unpause access control follows standard pattern.The pause/unpause functions use role-based access control with
PAUSER_ROLEfor pausing andGOVERNANCE_ROLEfor unpausing. This asymmetry (easier to pause than unpause) is a good security practice for emergency response.
150-157: Let me search the codebase directly for the fee increase implementations:I need to examine the actual implementations in the switchboard contracts. Let me run a script to inspect the fee increase functions:
Based on the script execution output and analysis, I can now generate the final response:
Switchboard implementations properly validate payload ownership via
payloadIdToPlugmapping.The concern is addressed by the switchboard contracts. The script output shows that
EVMxSwitchboardvalidates payload ownership with:if (payloadIdToPlug[payloadId_] != plug_) revert InvalidSource();(line 168). TheMessageSwitchboard.increaseFeesForPayloadfunction is gated with theonlySocketmodifier (line 527), meaning only the Socket contract (which includesSocketUtils.sol) can call it. The architecture correctly implements the trust assumption:SocketUtilsverifies the caller is a connected plug, then delegates to the switchboard, which independently validates that the plug owns the specific payloadId before processing fees.contracts/protocol/switchboard/SwitchboardBase.sol (3)
40-48: Constructor validation prevents misconfiguration.The constructor now validates that
chainSlug,socket_, andowner_are all non-zero before initialization. This prevents deployment with invalid configuration.
60-71: Transmitter signature is optional and not used for authorization.The
getTransmitterfunction returnsaddress(0)when no signature is provided. The comment on line 58 notes that "Recovered signer should be validated for valid roles by caller." This means the transmitter signature is optional and used only for accountability/reputation, not for authorization.This is consistent with the audit documentation stating that transmitter signatures are optional and don't affect authorization (only attestations matter).
81-87: Signature recovery uses standard Ethereum signed message format.The
_recoverSignerfunction properly applies the Ethereum signed message prefix (\x19Ethereum Signed Message:\n32) before recovering the signer. This prevents signature replay between different contexts (e.g., raw message vs. signed message).auditor-docs/AUDIT_PREP_SUMMARY.md (1)
1-329: Documentation is comprehensive and audit-ready.The audit preparation summary effectively documents the nonce namespace isolation implementation, validated security patterns, and dismissed non-issues. The content aligns with the actual code changes in MessageSwitchboard.sol.
auditor-docs/SECURITY_MODEL.md (1)
1-480: Comprehensive security documentation.The security model documentation provides detailed trust assumptions, access control matrix, critical invariants, and attack surface analysis. This is well-structured and audit-appropriate.
auditor-docs/MESSAGE_FLOW.md (1)
1-614: Detailed flow documentation.The message flow documentation provides comprehensive step-by-step descriptions of outbound, inbound, attestation, and fee management flows. This aligns well with the contract implementations.
contracts/utils/common/Structs.sol (2)
32-43: LGTM! Escrow state tracking structures are well-defined.The EscrowState enum and EscrowEntry struct provide clear state management for escrow functionality.
83-267: Struct definitions support the new payload architecture.The updated ExecutionParams, DigestParams, and new fee tracking structs (PayloadFees, SponsoredPayloadFees, MessageOverrides) align with the payload-centric refactor. Definitions are syntactically correct.
contracts/protocol/switchboard/EVMxSwitchboard.sol (3)
15-79: LGTM! State variables and constructor are properly initialized.The immutable EVMX configuration and state mappings are well-structured. Default deadline of 1 day is reasonable.
90-100: LGTM! Attestation logic is secure.The attest function properly prevents double attestation, verifies watcher role, and records attestation state.
129-153: LGTM! Payload processing is implemented correctly.The processPayload function validates configuration, handles deadline overrides appropriately, and generates unique payload IDs.
contracts/protocol/SocketConfig.sol (3)
60-70: LGTM! Switchboard registration is correctly implemented.The registration logic properly prevents duplicate registration and assigns unique IDs.
134-140: LGTM! Disconnect properly clears plug connection.The disconnect function correctly validates the plug is connected before clearing the mapping.
171-181: LGTM! getPlugConfig correctly handles disconnected plugs.The early return at line 176 when
switchboardId == 0properly prevents calling address(0), addressing the concern raised in past reviews.contracts/protocol/Socket.sol (4)
46-73: LGTM! Execute function has proper validation flow.The execute function correctly validates deadline, call type, plug connection, msg.value, payload ID routing, execution status, and digest before executing the payload.
83-106: LGTM! Verification logic is correctly implemented.The _verify function properly recovers transmitter, creates digest, stores it, and validates with switchboard. The parameter naming issue from past reviews has been resolved.
116-149: LGTM! Execution logic properly handles success and failure.The _execute function correctly checks gas limits, performs external call with gas limits, and delegates to appropriate handlers based on success/failure.
216-254: LGTM! Outbound payload flow is correctly implemented.The sendPayload, _sendPayload, and fallback functions properly handle payload submission. The double encoding in fallback is well-documented and necessary for ABI compatibility.
contracts/protocol/switchboard/MessageSwitchboard.sol (10)
1-132: LGTM! Contract properly extends ReentrancyGuard.The import of SafeTransferLib and ReentrancyGuard, plus inheritance at line 19, addresses the past review concern about reentrancy protection for the refund path.
171-240: LGTM! Payload processing correctly uses deadline.The processPayload function properly decodes overrides, validates sibling configuration, and uses the deadline at line 338 in digestParams creation. The concern from past reviews about unused deadline has been addressed.
249-305: LGTM! Override decoding handles both native and sponsored flows.The _decodeOverrides function correctly decodes version 1 (native token) and version 2 (sponsored) override formats.
307-365: LGTM! Helper functions are correctly implemented.The _validateSibling, _createDigestAndPayloadId, and _validateAndUseNonce functions properly validate configuration, create identifiers, and implement nonce namespace isolation.
371-405: LGTM! Plug approval and revocation logic is straightforward.The sponsor approval functions correctly manage plug approvals and emit appropriate events.
414-474: LGTM! Attestation and refund functions are secure.The attest function properly verifies watcher signatures. The markRefundEligible function uses nonce-based replay protection. The refund function correctly uses nonReentrant and follows CEI pattern.
484-579: LGTM! Fee management functions are correctly implemented.The batch fee update uses proper length prefixes for collision resistance. The fee increase functions validate plug ownership and handle both native and sponsored flows appropriately.
588-617: LGTM! Source decoding and payload verification are correct.The _decodePackedSource function correctly extracts chainSlug and plug from packed bytes using assembly. The allowPayload function properly validates sibling configuration and attestation status.
623-650: LGTM! Digest creation uses proper length prefixes.The _createDigest function correctly includes length prefixes for variable-length fields (payload, source, extraData) to prevent collision attacks.
677-691: LGTM! Deadline and config getters are correctly implemented.The setDefaultDeadline and getPlugConfig functions properly handle deadline configuration and plug config retrieval.
|
|
||
| ### Core Assumptions | ||
|
|
||
| **A1: Switchboards are trusted by Plugs/Apps** |
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
Use markdown headings instead of bold text for section headers.
Bold text (**Text**) doesn't create proper document structure. Convert all section headers to markdown headings (e.g., ### Text). This affects TOC generation, outline view, and accessibility. Examples: lines 7, 13, 18, 23, 29, 35 should use #### (fourth-level heading) within the "System Assumptions" section.
Also applies to: 13-13, 18-18, 23-23, 29-29, 35-35, 320-320, 328-328, 848-848, 859-859, 871-871, 889-889, 897-897, 903-903
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
7-7: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🤖 Prompt for AI Agents
In auditor-docs/FAQ.md around lines 7 to 7 (and also apply to lines 13, 18, 23,
29, 35, 320, 328, 848, 859, 871, 889, 897, 903), replace the current bolded
section titles (e.g., **A1: Switchboards are trusted by Plugs/Apps**) with
proper Markdown fourth-level headings by removing the **bold markers** and
prefixing the line with "#### " so the header text remains identical but becomes
a semantic heading; ensure spacing is correct and no extra bold formatting
remains so TOC/outlines and accessibility work correctly.
| ``` | ||
| Block N contains: | ||
| Tx1: plug.connect(switchboardId) | ||
| Tx2: governance.disableSwitchboard(switchboardId) | ||
| ``` | ||
|
|
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
Specify language identifiers for fenced code blocks.
Code blocks should declare their language for proper syntax highlighting. Line 401 uses a BlockN contains: structure (use plaintext/text). Lines 452, 460, 467 show fee distribution (use plaintext). Replace ``` with ```plaintext or ```solidity as appropriate. This improves readability and tooling compatibility.
Also applies to: 452-457, 460-465, 467-470
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
401-401: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In auditor-docs/FAQ.md around lines 401-406 (and also apply to ranges 452-457,
460-465, 467-470), the fenced code blocks lack language identifiers; update each
opening fence from ``` to a language-tagged fence (for the "Block N contains:"
and fee-distribution snippets use ```plaintext or ```text) and if any snippets
are actual Solidity code use ```solidity; ensure each block's opening fence is
updated accordingly while leaving the block contents unchanged.
| **For Audit Questions**: | ||
| - Open issue in repository with [AUDIT] tag | ||
| - Email: [[email protected]] | ||
| - Discord: [#auditor-support channel] | ||
|
|
||
| **For Technical Clarifications**: | ||
| - Reference this FAQ first | ||
| - Check other documentation files | ||
| - Ask in audit communication channel | ||
|
|
||
| **For Security Issues**: | ||
| - DO NOT post publicly | ||
| - Email: [[email protected]] | ||
| - Use PGP key if available | ||
|
|
||
| --- | ||
|
|
||
| ## Document Updates | ||
|
|
||
| This FAQ is maintained during the audit process. If you have questions not covered here, please ask - we'll add them to help future auditors. | ||
|
|
||
| **Last Updated**: [Date] | ||
| **Version**: 1.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.
🧩 Analysis chain
Remove or complete placeholder contact information and metadata.
Lines 1095-1117 contain example/placeholder values: [AUDIT] tag template, [[email protected]], [#auditor-support channel], [[email protected]], and [Date] placeholder. Either fill these with actual contact details or remove this section before merge. Auditors will see fake addresses if left as-is.
Is this contact section intended for this version, or should it be removed until real details are available?
🏁 Script executed:
#!/bin/bash
# Check if FAQ.md exists and view the actual content at the flagged lines
if [ -f "auditor-docs/FAQ.md" ]; then
echo "=== FAQ.md content around lines 1095-1117 ==="
sed -n '1090,1120p' auditor-docs/FAQ.md | cat -n
else
echo "File not found: auditor-docs/FAQ.md"
echo "=== Searching for FAQ files ==="
find . -name "*FAQ*" -o -name "*faq*" 2>/dev/null | head -20
fiLength of output: 874
🏁 Script executed:
#!/bin/bash
# Search for actual contact information and security email patterns in the codebase
echo "=== Searching for security contact information ==="
rg -i "security.*email|contact.*security|audit.*email" -C 2 --type=md --type=txt 2>/dev/null | head -50
echo "=== Searching for Discord/chat channel info ==="
rg -i "discord|slack.*channel|communication.*channel" -C 2 --type=md 2>/dev/null | head -30
echo "=== Checking for placeholder/TODO markers ==="
rg "\[Date\]|\[AUDIT\]|TODO|FIXME.*contact" -C 1 --type=md 2>/dev/null | head -40Length of output: 1529
🏁 Script executed:
#!/bin/bash
# Check git status and recent changes to FAQ.md to understand intent
echo "=== Git log for FAQ.md (last 5 commits) ==="
git log --oneline -5 -- auditor-docs/FAQ.md 2>/dev/null || echo "Git not available"
echo "=== Git diff for FAQ.md (staged/uncommitted changes) ==="
git diff HEAD -- auditor-docs/FAQ.md 2>/dev/null | head -100 || echo "No staged changes"Length of output: 158
This section must be completed with real contact details before merge or removed entirely.
The placeholders are confirmed: [AUDIT] tag, [[email protected]], [#auditor-support channel], [[email protected]], and [Date] all remain unfilled in the actual file. Auditors trying to reach support via these fake addresses will fail. Either fill these with actual team contact information or delete the Contact & Support section until you're ready to publish real details.
The same pattern exists in auditor-docs/README.md and auditor-docs/AUDIT_PREP_SUMMARY.md — those need the same treatment.
🤖 Prompt for AI Agents
In auditor-docs/FAQ.md around lines 1095 to 1117, the Contact & Support section
currently contains placeholder tags and fake addresses; replace these
placeholders with actual, verified contact details (real audit issue tag,
operational support email, actual Discord channel name/link, security reporting
email, and the real last-updated date and version) or remove the entire Contact
& Support block if you cannot provide real details yet; apply the same fix to
auditor-docs/README.md and auditor-docs/AUDIT_PREP_SUMMARY.md so no placeholder
contact information remains in the repository.
| // Call depositFromChain through interface (goes to Socket's fallback) | ||
| bytes memory payloadIdBytes = IGasAccountManager(address(socket__)).depositFromChain( | ||
| token_, | ||
| receiver_, | ||
| gasAmount_, | ||
| nativeAmount_ | ||
| ); | ||
|
|
||
| // DECODING: Socket's fallback returns abi.encode(abi.encode(payloadId)) | ||
| // Using interface call, Solidity auto-decodes the outer ABI layer, payloadIdBytes contains: 32 bytes (the payloadId) | ||
| payloadId = abi.decode(payloadIdBytes, (bytes32)); | ||
| token_.safeTransferFrom(msg.sender, address(this), gasAmount_ + nativeAmount_); | ||
| emit GasDeposited(token_, receiver_, gasAmount_, nativeAmount_, 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.
🛠️ Refactor suggestion | 🟠 Major
Simplify after fixing interface type.
Once the IGasAccountManager interface returns bytes32 directly (as flagged above), this code should be simplified to:
- // Call depositFromChain through interface (goes to Socket's fallback)
- bytes memory payloadIdBytes = IGasAccountManager(address(socket__)).depositFromChain(
+ payloadId = IGasAccountManager(address(socket__)).depositFromChain(
token_,
receiver_,
gasAmount_,
nativeAmount_
);
-
- // DECODING: Socket's fallback returns abi.encode(abi.encode(payloadId))
- // Using interface call, Solidity auto-decodes the outer ABI layer, payloadIdBytes contains: 32 bytes (the payloadId)
- payloadId = abi.decode(payloadIdBytes, (bytes32));
token_.safeTransferFrom(msg.sender, address(this), gasAmount_ + nativeAmount_);The double-encoding pattern adds unnecessary complexity.
📝 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.
| // Call depositFromChain through interface (goes to Socket's fallback) | |
| bytes memory payloadIdBytes = IGasAccountManager(address(socket__)).depositFromChain( | |
| token_, | |
| receiver_, | |
| gasAmount_, | |
| nativeAmount_ | |
| ); | |
| // DECODING: Socket's fallback returns abi.encode(abi.encode(payloadId)) | |
| // Using interface call, Solidity auto-decodes the outer ABI layer, payloadIdBytes contains: 32 bytes (the payloadId) | |
| payloadId = abi.decode(payloadIdBytes, (bytes32)); | |
| token_.safeTransferFrom(msg.sender, address(this), gasAmount_ + nativeAmount_); | |
| emit GasDeposited(token_, receiver_, gasAmount_, nativeAmount_, payloadId); | |
| payloadId = IGasAccountManager(address(socket__)).depositFromChain( | |
| token_, | |
| receiver_, | |
| gasAmount_, | |
| nativeAmount_ | |
| ); | |
| token_.safeTransferFrom(msg.sender, address(this), gasAmount_ + nativeAmount_); | |
| emit GasDeposited(token_, receiver_, gasAmount_, nativeAmount_, payloadId); |
🤖 Prompt for AI Agents
In contracts/evmx/plugs/GasStation.sol around lines 87 to 99, the code currently
treats IGasAccountManager.depositFromChain as returning bytes memory and then
abi.decoding a bytes32 payload, but after fixing the IGasAccountManager
interface to return bytes32 directly you should simplify the call: change the
local variable type to bytes32, assign payloadId directly from the interface
call (no abi.decode or intermediate bytes memory), and remove the comment about
double-encoding; keep the token_.safeTransferFrom and emit lines unchanged so
the flow remains the same.
| uint32 public immutable switchboardId; | ||
|
|
||
| /// @notice Mapping of payload ID to revert status (used by plugs to mark payloads as reverting) | ||
| mapping(bytes32 => bool) public revertingPayloadIds; |
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 the revertingPayloadIds mapping is actually used.
The previous review identified that revertingTriggers was dead code (written but never read). The mapping has been renamed to revertingPayloadIds, but we need to verify that this mapping is now actually read and used in the execution logic.
Run the following script to check if revertingPayloadIds is read anywhere:
🏁 Script executed:
#!/bin/bash
# Description: Verify revertingPayloadIds is actually used (read) in the codebase
# Search for reads of revertingPayloadIds (excluding writes)
echo "=== Reads of revertingPayloadIds ==="
rg -n 'revertingPayloadIds\[' --glob '*.sol' -A 2 -B 2 | grep -v 'revertingPayloadIds\[.*\]\s*='
# Look for conditional checks
echo "=== Conditional checks ==="
rg -n 'if.*revertingPayloadIds|require.*revertingPayloadIds' --glob '*.sol'
# Search for any function that reads this mapping
echo "=== Functions that might read this ==="
rg -n -B5 -A10 'function.*execute|function.*allow|function.*verify' --glob '*.sol' | rg -B5 -A10 'revertingPayloadIds'Length of output: 873
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# More comprehensive search for ANY access to revertingPayloadIds (reads or writes)
echo "=== All revertingPayloadIds accesses ==="
rg -n 'revertingPayloadIds' --glob '*.sol' -B 2 -A 2
echo ""
echo "=== Count by file ==="
rg -l 'revertingPayloadIds' --glob '*.sol' | xargs -I {} bash -c 'echo "{}"; rg -c "revertingPayloadIds" "{}"'
echo ""
echo "=== Search for actual conditionals that might use it ==="
rg -n -B10 -A10 'revertingPayloadIds' --glob '*.sol' | grep -E '(if|require|assert|revert|return)' -A 2 -B 2Length of output: 9437
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Check if execution/verification logic should be using revertingPayloadIds
echo "=== allowPayload functions that might check reverting status ==="
rg -n -A20 'function allowPayload' --glob '*.sol' | head -100
echo ""
echo "=== Check MessageSwitchboard allowPayload specifically ==="
rg -n -A30 'allowPayload.*payloadId' --glob 'MessageSwitchboard.sol'Length of output: 8039
Remove the unused revertingPayloadIds mapping or implement its verification logic.
The mapping is written via setRevertingPayload() in both EVMxSwitchboard and MessageSwitchboard but never read anywhere. Both allowPayload() implementations only check isAttested[digest_] and source validation—they completely ignore the reverting status. This is the same dead code problem as the original revertingTriggers mapping, just renamed.
Either remove the mapping entirely (saves gas, eliminates false API surface) or implement the actual check in allowPayload() to reject reverting payloads. Currently watchers can mark payloads as reverting but the switchboard ignores it.
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/SwitchboardBase.sol around line 28, the
mapping `revertingPayloadIds` is written by `setRevertingPayload()` but never
read, leaving dead code; either remove this mapping and its setter to save gas
and avoid a misleading API, or implement the verification in `allowPayload()` to
consult `revertingPayloadIds[digest_]` and reject payloads marked as reverting.
If you choose to implement the check, add a require or conditional reject in
`allowPayload()` that returns false or reverts when
`revertingPayloadIds[digest_]` is true, and update any related
documentation/events/tests; if you choose removal, delete the mapping, its
setter, and any associated storage references and tests.
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: 1
♻️ Duplicate comments (3)
contracts/protocol/switchboard/MessageSwitchboard.sol (2)
141-152: Sibling config setter still lacks zero‑value validation
setSiblingConfigstoressocket_,switchboard_, andswitchboardId_without checking for zero values. If any of these are accidentally set to zero, later operations (e.g._validateSibling/_createDigestAndPayloadId) will revert withSiblingSocketNotFound, but only after configuration has been “accepted”.This should validate inputs up front:
function setSiblingConfig( uint32 chainSlug_, bytes32 socket_, bytes32 switchboard_, uint32 switchboardId_ ) external onlyOwner { + if (socket_ == bytes32(0) || switchboard_ == bytes32(0) || switchboardId_ == 0) { + revert SiblingSocketNotFound(); + } siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }This catches misconfiguration early and keeps sibling maps consistent.
636-671: Plug config encoding betweenupdatePlugConfigandgetPlugConfigis inconsistent
updatePlugConfigdecodes itsplugConfig_argument as:(uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32));and uses
siblingChainSlugas the key intosiblingPlugs.However,
getPlugConfigreturns:uint32 chainSlug_ = abi.decode(extraData_, (uint32)); plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]);so the encoded
plugConfig_is just a singlebytes32, not a(uint32, bytes32)tuple.If
getPlugConfigis intended to produce the bytes that remote components pass intoupdatePlugConfig, this mismatch will causeabi.decode(plugConfig_, (uint32, bytes32))to revert or read garbage. At the same time,updatePlugConfigstill lacks a validation thatsiblingPlugis non‑zero, so misconfigurations can silently writebytes32(0)and later cause_validateSiblingto revert.This should be aligned one way or the other, for example:
- Make them symmetric:
function getPlugConfig(...) external view override returns (bytes memory plugConfig_) { uint32 chainSlug_ = abi.decode(extraData_, (uint32)); - plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]); + plugConfig_ = abi.encode(chainSlug_, siblingPlugs[chainSlug_][plug_]); } function updatePlugConfig(...) external override onlySocket { - (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); + (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); ... }
- Or, if the chainSlug is always supplied separately (via
extraData_on the caller side), changeupdatePlugConfigto decode onlybytes32and derive the chainSlug from context.Additionally, add a non‑zero check for
siblingPlugto avoid writing unusable configs:if ( siblingSockets[siblingChainSlug] == bytes32(0) || siblingSwitchboards[siblingChainSlug] == bytes32(0) ) { revert SiblingSocketNotFound(); } + if (siblingPlug == bytes32(0)) { + revert SiblingSocketNotFound(); + }#!/bin/bash # Inspect all usages of getPlugConfig/updatePlugConfig to confirm the expected wire format. rg -n "getPlugConfig" contracts -C3 rg -n "updatePlugConfig" contracts -C3contracts/protocol/Socket.sol (1)
181-194: ImportSafeTransferLibor this will not compile
_handleFailedExecutionusesSafeTransferLib.safeTransferETH(receiver, msg.value);butcontracts/protocol/Socket.soldoes not importSafeTransferLib. Solidity will fail to resolve this symbol.This should add the import at the top of the file:
import "./SocketUtils.sol"; import {WRITE} from "../utils/common/Constants.sol"; import "../utils/common/Errors.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; using LibCall for address;The rest of
_handleFailedExecution(marking status asRevertedand refunding fullmsg.valuetorefundAddressormsg.sender) is consistent with the pre‑executionmsg.valuecheck inexecute.Run a quick compile or search to confirm the missing import and fix:
#!/bin/bash # From repo root rg -n "SafeTransferLib.safeTransferETH" contracts/protocol/Socket.sol -n rg -n "import {SafeTransferLib}" contracts -n || echo "No SafeTransferLib import in Socket.sol" # Then compile with your usual toolchain # forge build # or: npx hardhat compile / pnpm test
🧹 Nitpick comments (5)
contracts/protocol/Socket.sol (3)
116-149: Gas guard and external call pattern are mostly solidThe gas buffer check using
gasLimitBufferbeforetryCalland the separation of success vs failure handling keep the external plug call well‑contained. State writes (executionStatus) are done before the external call to block reentrant double‑execution for the samepayloadId_within a single tx, while_handleFailedExecutiontoggles status toRevertedto allow future retries.If you want execute() to be robust even when a misconfigured plug reenters into other entrypoints, this should eventually be paired with a global reentrancy guard, but that is a design hardening rather than a correctness bug.
158-172: Fee collection ordering is correct but creates a hard dependency onnetworkFeeCollectorOn success you emit
ExecutionSuccessand then forwardsocketFeestonetworkFeeCollectorif configured. This ensures network fees are only charged when the plug call succeeds, but any revert incollectNetworkFeewill roll back the entire execution despite the plug having already run successfully.If you want to avoid “plug succeeded but fee collector misconfiguration bricks execution”, this should wrap
collectNetworkFeein atry/catch(or equivalent) and treat fee collection failure as non‑fatal.
201-206: ExecutionStatus semantics allow retries after failure; comment matches behavior
_validateExecutionStatusonly blocks when status isExecutedand sets that state upfront._handleFailedExecutionoverwrites it toReverted, so a payload can be retried in a later tx if it previously reverted, but not once it has ever completed successfully. This also blocks reentrant double‑execution of the same payloadId within a single tx.If the intent is “retry until success, then lock forever”, this is correct. If you instead want any attempted execution (even a revert) to be terminal, this should treat any non‑zero status as final.
contracts/protocol/switchboard/MessageSwitchboard.sol (2)
171-240: Payload processing flow is sound; consider tightening sponsored/native value semantics
processPayloadcleanly decodes overrides, applies a default deadline when absent, validates sibling config, derives(digestParams, digest, payloadId)once, and then branches into sponsored vs native fee handling. The checks on sponsor approvals andmsg.value >= minMsgValueFees[dst] + overrides.value(for native) are appropriate.Two design points to consider:
- In the sponsored branch,
msg.valueis accepted but ignored. If sponsored flows are intended to be pure‑message (no native value), this should either enforcemsg.value == 0or clearly document and handle any native value so it is not accidentally stranded.- For native flows, you treat the full
msg.valueasnativeFeeseven though part of it may conceptually correspond tooverrides.value. If other components interpretnativeFeesas “refundable fees only”, this should be split and tracked explicitly.These are semantics/design clarifications rather than immediate correctness bugs.
652-660: Default deadline setter is fine but should probably forbid zero
setDefaultDeadlineis owner‑only and simply assigns the newdefaultDeadline. SinceprocessPayloadusesblock.timestamp + defaultDeadlinewhen overrides don’t specify a deadline, setting this to zero effectively means “default deadline is now”, which makes any such payload immediately expire on the destination duringexecute.This should either reject a zero
defaultDeadline_or treat it as a sentinel with clearly documented semantics.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/protocol/Socket.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(3 hunks)
🔇 Additional comments (20)
contracts/protocol/Socket.sol (6)
16-34: Execution status mapping and gas buffer initialization look sound
executionStatuscleanly replaces the boolean flag and, together with_validateExecutionStatus/_handleFailedExecution, gives a clear per‑payload lifecycle. InitializinggasLimitBufferto 105 in the constructor matches the gas guard in_executeand keeps the configuration on-chain/governable if needed later.
46-73: Execute flow and validation ordering are reasonableDeadline, callType, plug connection,
msg.value, payloadId, executionStatus, and digest are validated before doing the external call via_execute, and the whole entrypoint iswhenNotPaused. This gives a solid precondition wall around the untrusted plug call without obvious gaps.
83-106: Digest verification throughISwitchboardis coherentPassing
switchboardAddressexplicitly into_verify, derivingtransmitterviagetTransmitter, and then persisting the digest inpayloadIdToDigestbefore callingallowPayloadis a clean separation of concerns. It ensures the switchboard both originates and authorizes the digest tied topayloadId, target, and source.
216-240: Outbound send path via_sendPayloadis consistent with switchboard design
sendPayloadforwarding(plug = msg.sender, value = msg.value, callData)into_sendPayloadand then intoISwitchboard.processPayload{value: value_}with plug overrides cleanly centralizes outbound routing. ThewhenNotPausedon_sendPayloadalso ensures both direct and fallback calls are pause‑aware.
260-266: Rejecting plain ETH viareceiveis consistent with the rest of the APIReverting in
receive()with a clear message forces all ETH inflows to go throughexecuteorsendPayload, which is important for keeping accounting of fees and refunds correct.
251-258: Review comment is incorrectThe claimed ABI encoding layout is wrong. When
abi.encode()encodes a dynamicbytesargument, it stores: offset, length, and then the data. Forabi.encode(abi.encode(payloadId)):
- Inner
abi.encode(payloadId)producesbytes memorywith content:[length field: 32 bytes][payloadId: 32 bytes]= 64 bytes total- Outer
abi.encode(bytes)encodes this 64-byte dynamic bytes as:[offset: 0x20][length: 0x40][64 bytes of inner encoding]The outer encoding has
length = 0x40(64 in decimal), not0x20(32) as claimed. The code'sabi.encodePacked(0x20, 0x20, payloadId)does not match the actual double-encoding layout and is not ABI-compatible withabi.encode(abi.encode(payloadId)).Likely an incorrect or invalid review comment.
contracts/protocol/switchboard/MessageSwitchboard.sol (14)
2-57: State surface and fee/attestation tracking are structured wellThe added mappings (
isAttested, sibling configs, minMsgValueFees, payloadFees, sponsoredPayloadFees, sponsorApprovals, usedNonces) andpayloadCounter/defaultDeadlinegive a clear, composable model for per‑payload accounting and watcher attestations. This is a good foundation for the new gas/payload architecture.
60-124: Event set covers the key protocol transitionsThe events emitted for outbound messages, sibling config, sponsor approvals, refunds, fee changes, payload requests, reverting payloads, and defaultDeadline changes provide sufficient observability for off‑chain infra and operators. Names and indexed fields look appropriate.
154-160: Reverting payload flagging is straightforward
setRevertingPayloadis restricted toWATCHER_ROLEand only flips an internal flag plus emits an event. No external calls or value transfers here, so the shape is safe.
249-305: Versioned override decoding and deadline threading look correct
_decodeOverridescleanly distinguishes version 1 (native + refundAddress) and version 2 (sponsored + sponsor/maxFees), fills theMessageOverridesstruct fields explicitly, and defers toprocessPayloadto backfill a default deadline when zero. Using a typed struct here significantly reduces decoding mistakes.
317-349: Digest and payloadId creation are consistent with sibling config
_createDigestAndPayloadId:
- Enforces a non‑zero
dstSwitchboardId.- Uses
createPayloadId(chainSlug, switchboardId, overrides_.dstChainSlug, dstSwitchboardId, payloadCounter++), which correctly encodes (src socket, src switchboardId, dst, dstId, pointer).- Builds
DigestParamswith:
socket=siblingSockets[dst]target=siblingPlugs[dst][plug_]source=abi.encodePacked(chainSlug, toBytes32Format(plug_))deadline,gasLimit,valuefrom overridescallType = WRITEThis matches the new allowPayload/source encoding and keeps the digest construction centralized.
357-365: Nonce namespacing is well‑designed
_validateAndUseNoncenamespaces nonces by function selector and signer, and storeskeccak256(selector, nonce)underusedNonces[signer]. This prevents cross‑function replay and signer confusion without complicating call sites.
371-387: Sponsor plug approvals are simple and adequate
approvePlugs/revokePlugsloop over provided plug arrays, directly togglesponsorApprovals[msg.sender][plug], and emit events. This keeps the approval model per‑sponsor/per‑plug and avoids any hidden global coupling.
396-411: Attestation flow is minimal but correct
attestrecovers a watcher fromkeccak256( toBytes32Format(address(this)), chainSlug, digest_ ), enforcesWATCHER_ROLE, rejects double attestations, and setsisAttested[digest_] = truewith an event. This provides a clean 1‑bit “digest is approved” flag thatallowPayloadcan consume.
464-493: Signed batch fee updates and nonce handling are robust
setMinMsgValueFeesBatch:
- Checks array length equality.
- Includes explicit length prefixes for both arrays in the signed message to prevent concatenation attacks.
- Uses
toBytes32Format(address(this)),chainSlug, arrays, andnonce_in the digest.- Verifies
FEE_UPDATER_ROLEon the recovered signer and calls_validateAndUseNoncewith a namespaced nonce.- Applies the updates in a simple loop with
MinMsgValueFeesSetevents.This is a solid pattern for off‑chain oracle/relayer fee updates.
503-539: Fee increase hooks respect plug ownership and are side‑effect minimal
increaseFeesForPayloaddispatches on a smallfeesTypeenum and hands off to_increaseNativeFees/_increaseSponsoredFees. Both internal functions:
- Load the correct fee struct.
- Enforce that
fees.plug == plug_to ensure only the originating plug can increase fees.- Update only the relevant amount (
nativeFees += msg.valueormaxFees = newMaxFees).- Emit corresponding events.
This keeps the “who can top up” rule simple and avoids accidental cross‑payload modifications.
587-597: allowPayload correctly binds digest to configured sibling plugBy decoding
sibling_into(siblingChainSlug, siblingPlug)and requiringsiblingPlugs[siblingChainSlug][target_] == siblingPlug,allowPayloadensures that:
- The
target_on the destination chain matches the plug configured for the encoded source pair, and- The digest has been attested (
isAttested[digest_]).This tightly couples attestation to the configured sibling topology and makes spoofing the source plug difficult.
603-630: Digest construction uses length prefixes to avoid ambiguity
_createDigestsplits fixed‑size and variable‑size fields, using:
abi.encodePackedfor the fixed portion, and- explicit
uint32(length)prefixes beforepayload,source, andextraData.This should prevent common length‑extension / concatenation collisions in the digest preimage.
665-671: getPlugConfig extraData usage is clearDecoding
extraData_as auint32 chainSlug_and returning the encoded sibling plug for that chain gives the caller flexibility to request per‑chain config. Once the encoding mismatch withupdatePlugConfigis resolved, this should be a straightforward way to sync plug mappings across chains.
568-582: Packed source decoding verified and correct
abi.encodePacked(uint32, bytes32)produces 36 bytes with the uint32 occupying the first 4 bytes (unpacked) and bytes32 occupying the next 32 bytes. The assembly correctly recovers both values: right-shifting by 224 bits extracts the uint32, and reading 32 bytes from offset 4 in the data region recovers the bytes32. This implementation matches the encoding layout.
| function markRefundEligible( | ||
| bytes32 payloadId_, | ||
| uint256 nonce_, | ||
| bytes calldata signature_ | ||
| ) external { | ||
| PayloadFees storage fees = payloadFees[payloadId_]; | ||
| if (fees.isRefundEligible) revert AlreadyMarkedRefundEligible(); | ||
| if (fees.nativeFees == 0) revert NoFeesToRefund(); | ||
| bytes32 digest = keccak256( | ||
| abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, nonce_) | ||
| ); | ||
| address watcher = _recoverSigner(digest, signature_); | ||
| if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); | ||
|
|
||
| _validateAndUseNonce(this.markRefundEligible.selector, watcher, nonce_); | ||
|
|
||
| fees.isRefundEligible = true; | ||
| emit RefundEligibilityMarked(payloadId_, watcher); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Claim refund for a payload | ||
| * @param payloadId_ Payload ID to refund | ||
| */ | ||
| function refund(bytes32 payloadId_) external nonReentrant { | ||
| PayloadFees storage fees = payloadFees[payloadId_]; | ||
| if (!fees.isRefundEligible) revert RefundNotEligible(); | ||
| if (fees.isRefunded) revert AlreadyRefunded(); | ||
|
|
||
| uint256 feesToRefund = fees.nativeFees; | ||
| fees.isRefunded = true; | ||
| fees.nativeFees = 0; | ||
|
|
||
| emit Refunded(payloadId_, fees.refundAddress, feesToRefund); | ||
| SafeTransferLib.safeTransferETH(fees.refundAddress, feesToRefund); | ||
| } |
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.
Refunds to address(0) will permanently burn ETH
markRefundEligible and refund operate purely on PayloadFees and never validate fees.refundAddress. If a payload was created with refundAddress = address(0) (whether by bug, misconfiguration, or malicious plug), refund will successfully:
- mark
fees.isRefunded = true, - set
fees.nativeFeesto 0, - and call
SafeTransferLib.safeTransferETH(fees.refundAddress, feesToRefund),
sending all refundable ETH to the zero address.
Given that _decodeOverrides for native flow accepts an arbitrary refundAddress and processPayload stores it without checks, this should defensively reject the zero address to avoid irreversible fund loss. For example:
// In processPayload native branch, before writing payloadFees:
- payloadFees[payloadId] = PayloadFees({
+ if (overrides.refundAddress == address(0)) revert InvalidRefundAddress();
+ payloadFees[payloadId] = PayloadFees({
nativeFees: msg.value,
refundAddress: overrides.refundAddress,
isRefundEligible: false,
isRefunded: false,
plug: plug_
});Alternatively, you could mirror Socket._handleFailedExecution semantics and treat a zero refundAddress as msg.sender or the originating plug, but silently burning ETH is not acceptable in a failure/refund path.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/MessageSwitchboard.sol around lines 419–454,
the refund flow allows refunds to address(0) which would burn ETH; add a
defensive check to reject zero refund addresses: when marking refund eligibility
(markRefundEligible) verify payloadFees[payloadId_].refundAddress != address(0)
and revert with a new descriptive error (e.g., RefundToZeroAddress) if it is
zero, and also add the same check in refund() just before transferring to ensure
no transfer to address(0) occurs (or alternatively route the refund to a safe
fallback address like the original sender if you prefer), keeping state updates
and event emission behavior unchanged otherwise.
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: 3
♻️ Duplicate comments (8)
contracts/protocol/switchboard/EVMxSwitchboard.sol (1)
139-144: Simplify deadline logic and fix formatting.Two issues:
- Missing space in
overrides_.length==0(line 139)- The ternary
deadline > 0 ? deadline: defaultDeadline(line 143) is confusing. When would a caller explicitly encode0as the deadline? Either remove the ternary or document why0should map todefaultDeadline.bytes memory overrides; -if (overrides_.length==0) { +if (overrides_.length == 0) { overrides = abi.encode(block.timestamp + defaultDeadline); } else { uint256 deadline = abi.decode(overrides_, (uint256)); - overrides = abi.encode(block.timestamp + (deadline > 0 ? deadline: defaultDeadline)); + overrides = abi.encode(block.timestamp + deadline); }contracts/protocol/switchboard/MessageSwitchboard.sol (4)
436-447: Add address(0) validation in refund.While the root cause should be fixed in
processPayload, add a defensive check here to prevent burning ETH ifrefundAddressis somehow zero.Apply this diff:
function refund(bytes32 payloadId_) external nonReentrant { PayloadFees storage fees = payloadFees[payloadId_]; if (!fees.isRefundEligible) revert RefundNotEligible(); if (fees.isRefunded) revert AlreadyRefunded(); + if (fees.refundAddress == address(0)) revert InvalidRefundAddress(); uint256 feesToRefund = fees.nativeFees; fees.isRefunded = true; fees.nativeFees = 0; emit Refunded(payloadId_, fees.refundAddress, feesToRefund); SafeTransferLib.safeTransferETH(fees.refundAddress, feesToRefund); }
627-641: Validate siblingPlug is non-zero.The function doesn't validate that
siblingPlugis non-zero. A zero value would cause_validateSibling(line 310) to revert on subsequent calls.Apply this diff:
function updatePlugConfig( address plug_, bytes memory plugConfig_ ) external override onlySocket { (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); if ( siblingSockets[siblingChainSlug] == bytes32(0) || siblingSwitchboards[siblingChainSlug] == bytes32(0) ) { revert SiblingSocketNotFound(); } + if (siblingPlug == bytes32(0)) { + revert SiblingSocketNotFound(); + } siblingPlugs[siblingChainSlug][plug_] = siblingPlug; emit PlugConfigUpdated(plug_, siblingChainSlug, siblingPlug); }
141-152: Add validation for sibling configuration parameters.This function accepts
socket_,switchboard_, andswitchboardId_without validation. Zero values will causeSiblingSocketNotFounderrors in other functions (e.g., line 310).Apply this diff:
function setSiblingConfig( uint32 chainSlug_, bytes32 socket_, bytes32 switchboard_, uint32 switchboardId_ ) external onlyOwner { + if (socket_ == bytes32(0) || switchboard_ == bytes32(0) || switchboardId_ == 0) { + revert SiblingSocketNotFound(); + } siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }
217-223: Validate refundAddress to prevent ETH burn.The function stores
overrides.refundAddresswithout checking if it'saddress(0). If zero,refund()at line 446 will burn ETH viasafeTransferETH(address(0), ...).Apply this diff:
} else { // Native token flow - validate fees and track for potential refund if (msg.value < minMsgValueFees[overrides.dstChainSlug] + overrides.value) revert InsufficientMsgValue(); + if (overrides.refundAddress == address(0)) revert InvalidRefundAddress(); payloadFees[payloadId] = PayloadFees({ nativeFees: msg.value, refundAddress: overrides.refundAddress, isRefundEligible: false, isRefunded: false, plug: plug_ });contracts/protocol/SocketConfig.sol (1)
69-84: Validate switchboard existence before status changes.Both functions update status without verifying
switchboardAddresses[switchboardId_] != address(0). Enabling an unregistered ID will mark it asREGISTEREDbut point toaddress(0), causing "call to non-contract" errors later.Apply this diff to both functions:
function disableSwitchboard( uint32 switchboardId_ ) external onlyRole(SWITCHBOARD_DISABLER_ROLE) { + if (switchboardAddresses[switchboardId_] == address(0)) revert InvalidSwitchboard(); switchboardStatus[switchboardId_] = SwitchboardStatus.DISABLED; emit SwitchboardDisabled(switchboardId_); } function enableSwitchboard(uint32 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { + if (switchboardAddresses[switchboardId_] == address(0)) revert InvalidSwitchboard(); switchboardStatus[switchboardId_] = SwitchboardStatus.REGISTERED; emit SwitchboardEnabled(switchboardId_); }contracts/protocol/Socket.sol (2)
181-194: Add missing SafeTransferLib import.Line 191 calls
SafeTransferLib.safeTransferETHbut the library isn't imported. This will cause a compilation error.Apply this diff:
import "./SocketUtils.sol"; import {WRITE} from "../utils/common/Constants.sol"; import "../utils/common/Errors.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; using LibCall for address;
201-206: Prevent re-execution of reverted payloads.The function only blocks when status is
Executed, allowing re-execution when status isReverted. A payload marked as reverted at line 187 can be executed again, contradicting the intent.Apply this diff:
function _validateExecutionStatus(bytes32 payloadId_) internal { - if (executionStatus[payloadId_] == ExecutionStatus.Executed) - revert PayloadAlreadyExecuted(); + ExecutionStatus status = executionStatus[payloadId_]; + if (status != ExecutionStatus(0)) revert PayloadAlreadyExecuted(); executionStatus[payloadId_] = ExecutionStatus.Executed; }Assumes
ExecutionStatus(0)represents "not yet executed". Adjust enum if needed.
🧹 Nitpick comments (1)
contracts/protocol/SocketConfig.sol (1)
106-119: Add defensive check for switchboard address.While status check ensures
REGISTERED, ifenableSwitchboard(line 81) doesn't validate existence, this could calladdress(0). Add a defensive check.Apply this diff:
function connect(uint32 switchboardId_, bytes memory plugConfig_) external override { + address switchboardAddress = switchboardAddresses[switchboardId_]; if ( - switchboardId_ == 0 || switchboardStatus[switchboardId_] != SwitchboardStatus.REGISTERED + switchboardId_ == 0 || + switchboardAddress == address(0) || + switchboardStatus[switchboardId_] != SwitchboardStatus.REGISTERED ) revert InvalidSwitchboard(); plugSwitchboardIds[msg.sender] = switchboardId_; if (plugConfig_.length > 0) { - ISwitchboard(switchboardAddresses[switchboardId_]).updatePlugConfig( + ISwitchboard(switchboardAddress).updatePlugConfig( msg.sender, plugConfig_ ); } emit PlugConnected(msg.sender, switchboardId_, plugConfig_); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
contracts/protocol/Socket.sol(1 hunks)contracts/protocol/SocketConfig.sol(1 hunks)contracts/protocol/SocketUtils.sol(3 hunks)contracts/protocol/interfaces/ISocket.sol(1 hunks)contracts/protocol/switchboard/EVMxSwitchboard.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(3 hunks)contracts/utils/common/Constants.sol(1 hunks)
🔇 Additional comments (10)
contracts/protocol/switchboard/EVMxSwitchboard.sol (1)
136-136: Remove redundant runtime check after constructor validation.Once the constructor validates
evmxChainSlugandwatcherIdare non-zero (per earlier comment), this check becomes redundant and wastes gas on everyprocessPayloadcall. Immutable values can't change after deployment.-if (evmxChainSlug == 0 || watcherId == 0) revert EvmxConfigNotSet(); - bytes memory overrides;Likely an incorrect or invalid review comment.
contracts/utils/common/Constants.sol (1)
1-19: LGTM!Constant updates align with the broader refactoring. The new
GAS_LIMIT_BUFFERconstant (5% buffer) is used for gas estimation safety margins.contracts/protocol/SocketUtils.sol (2)
42-50: LGTM!Constructor validations are appropriate. The
gasLimitBuffer_ >= 100check ensures at least no reduction below the requested gas limit.
103-196: LGTM!The verification helpers, fee management, and pausable controls are well-structured. The off-chain simulation function properly restricts access and limits return data copying.
contracts/protocol/SocketConfig.sol (2)
91-96: LGTM!Setting
address(0)is intentional per the comment at line 89 to disable fee collection. No validation needed.
141-151: LGTM!The function properly handles disconnected plugs with the early return at line 146.
contracts/protocol/interfaces/ISocket.sol (1)
1-158: LGTM!Interface updates are comprehensive and well-documented. Event signatures and function declarations align with the payload-centric architecture refactoring.
contracts/protocol/Socket.sol (3)
46-73: LGTM!The execute function has comprehensive validation checks before delegating to internal execution. The flow is clear and secure.
158-172: LGTM!Success handler properly collects fees only when collector is configured. Event emission and fee handling are correct.
216-267: LGTM!Outbound payload flow is well-structured. The fallback's manual ABI encoding (lines 254-257) is unusual but properly documented for cross-contract compatibility.
| constructor( | ||
| uint32 chainSlug_, | ||
| ISocket socket_, | ||
| address owner_, | ||
| uint32 evmxChainSlug_, | ||
| uint32 watcherId_ | ||
| ) SwitchboardBase(chainSlug_, socket_, owner_) { | ||
| evmxChainSlug = evmxChainSlug_; | ||
| watcherId = watcherId_; | ||
| } |
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.
Validate constructor parameters to prevent unusable contract deployment.
The constructor doesn't validate that evmxChainSlug_ and watcherId_ are non-zero. If either is zero, every processPayload call will revert at line 136, making the contract unusable. Fail fast by validating in the constructor.
Apply this diff:
constructor(
uint32 chainSlug_,
ISocket socket_,
address owner_,
uint32 evmxChainSlug_,
uint32 watcherId_
) SwitchboardBase(chainSlug_, socket_, owner_) {
+ if (evmxChainSlug_ == 0 || watcherId_ == 0) revert EvmxConfigNotSet();
evmxChainSlug = evmxChainSlug_;
watcherId = watcherId_;
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/EVMxSwitchboard.sol around lines 72-81, the
constructor currently assigns evmxChainSlug_ and watcherId_ without validation;
add require checks to ensure evmxChainSlug_ != 0 and watcherId_ != 0 and revert
with descriptive messages (e.g., "evmxChainSlug must be non-zero" and "watcherId
must be non-zero") before assigning the state variables so the contract fails
fast and cannot be deployed unusable.
Feat/multi watcher
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
♻️ Duplicate comments (12)
contracts/evmx/base/AppGatewayBase.sol (1)
62-71: ResetisAsyncModifierSetin_postAsyncto close the async window.
_preAsync()setsisAsyncModifierSet = true, but_postAsync()never resets it. After the first async call,isAsyncModifierSetstays true permanently, so_setSchedule()will no longer be restricted to the intended async scope and can be called from any internal context that reaches it.This should close the async window per call by clearing the flag in
_postAsyncafter marking the promise valid:function _postAsync() internal { - _clearOverrides(); - address promise_ = watcher__().executePayload(); - isValidPromise[promise_] = true; + _clearOverrides(); + address promise_ = watcher__().executePayload(); + isValidPromise[promise_] = true; + // Close the async window for this call + isAsyncModifierSet = false; }This restores the guard’s usefulness and keeps
_setScheduletied to active async flows only.auditor-docs/FAQ.md (3)
7-40: Use markdown headings instead of bold for FAQ question labels.Lines like
**A1: Switchboards are trusted by Plugs/Apps**under “System Assumptions” use bold text as pseudo‑headings. This breaks document structure, TOC generation, and accessibility.This should convert these to proper headings, e.g.:
#### A1: Switchboards are trusted by Plugs/AppsApply similarly to A2–A6 and other bold‑only section titles flagged by markdownlint.
445-457: Add language identifiers to generic fenced code blocks.Several fenced blocks (e.g., “Block N contains:” and fee‑distribution diagrams) use plain triple backticks without a language, which triggers MD040 and reduces readability.
This should annotate them with an appropriate language, e.g.:
```plaintext Block N contains: Tx1: plug.connect(switchboardId) Tx2: governance.disableSwitchboard(switchboardId)User pays: msg.value ├─ executionParams.value → Plug ├─ transmissionParams.socketFees → NetworkFeeCollector └─ Remainder stays in MessageSwitchboard (excess/refund)Also applies to: 499-520
1211-1235: Replace placeholder contact details and metadata before release.The “Contact & Support” and “Document Updates” sections still contain placeholders:
[AUDIT]tag,[[email protected]],[#auditor-support channel],[[email protected]],**Last Updated**: [Date].This should be fixed before merge: either fill in real operational contacts (issue tag, email addresses, channel names, last‑updated date) or remove these sections until real details are available. Leaving fake addresses will misdirect auditors and security disclosures.
contracts/protocol/switchboard/MessageSwitchboard.sol (3)
153-164: Validate sibling configuration inputs to prevent latent misconfig.
setSiblingConfigwritessocket_,switchboard_, andswitchboardId_directly into storage without checking for zero values. A zero socket/switchboard/switchboardId will later triggerSiblingSocketNotFound()in_validateSiblingand_createDigestAndPayloadId, effectively bricking traffic for thatchainSlug_at runtime instead of failing fast during configuration.This should reject invalid inputs up front:
function setSiblingConfig( uint32 chainSlug_, bytes32 socket_, bytes32 switchboard_, uint32 switchboardId_ ) external onlyOwner { + if (socket_ == bytes32(0) || switchboard_ == bytes32(0) || switchboardId_ == 0) { + revert SiblingSocketNotFound(); + } siblingSockets[chainSlug_] = socket_; siblingSwitchboards[chainSlug_] = switchboard_; siblingSwitchboardIds[chainSlug_] = switchboardId_; emit SiblingConfigSet(chainSlug_, socket_, switchboard_); }
244-254: Refund toaddress(0)will irreversibly burn ETH.In the native flow:
processPayloadstoresrefundAddressunvalidated inpayloadFees[payloadId].refundAddress(lines 247–253).refundlater transfersfeesToRefundtofees.refundAddresswithout checking foraddress(0)(lines 477–483).If a plug or off‑chain caller sets
refundAddress = address(0), a successful refund will permanently burn all refundable ETH while markingisRefunded = true. This should never be silently allowed on a refund path.This should:
- Reject zero refund address at creation, or
- Map zero refund address to a safe fallback (e.g., plug or msg.sender) and document the behavior.
Example fix (strict rejection):
// In processPayload native branch, before writing payloadFees: - payloadFees[payloadId] = PayloadFees({ + if (overrides.refundAddress == address(0)) revert InvalidReceiver(); + payloadFees[payloadId] = PayloadFees({ nativeFees: msg.value, refundAddress: overrides.refundAddress, isRefundEligible: false, isRefunded: false, plug: plug_ });Optionally add a dedicated
InvalidRefundAddress()error ifInvalidReceiver()is not semantically appropriate.Also applies to: 472-483
663-677: Reject zero sibling plug inupdatePlugConfigto avoid unusable config.
updatePlugConfigdecodes(siblingChainSlug, siblingPlug)and only checks that sibling socket/switchboard are non-zero. It allowssiblingPlug == bytes32(0), which will later cause_validateSiblingto revert (sincedstPlug == bytes32(0)is treated as misconfigured sibling).This should validate
siblingPlug != bytes32(0)before writing:function updatePlugConfig( address plug_, bytes memory plugConfig_ ) external override onlySocket { (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); if ( siblingSockets[siblingChainSlug] == bytes32(0) || siblingSwitchboards[siblingChainSlug] == bytes32(0) ) { revert SiblingSocketNotFound(); } + if (siblingPlug == bytes32(0)) { + revert SiblingSocketNotFound(); + } siblingPlugs[siblingChainSlug][plug_] = siblingPlug; emit PlugConfigUpdated(plug_, siblingChainSlug, siblingPlug); }This keeps configuration errors from surfacing only at execution time.
contracts/protocol/switchboard/EVMxSwitchboard.sol (2)
83-92: Validate EVMX configuration in constructor to prevent unusable deployments.The constructor assigns
evmxChainSlug_andevmxWatcherId_to immutables without validation and relies onprocessPayloadto revert withEvmxConfigNotSet()when either is zero (line 153). If these values are misconfigured at deploy time, the contract becomes permanently unusable but only fails at runtime.This should fail fast in the constructor:
constructor( uint32 chainSlug_, ISocket socket_, address owner_, uint32 evmxChainSlug_, uint32 evmxWatcherId_ ) SwitchboardBase(chainSlug_, socket_, owner_) { + if (evmxChainSlug_ == 0 || evmxWatcherId_ == 0) revert EvmxConfigNotSet(); evmxChainSlug = evmxChainSlug_; evmxWatcherId = evmxWatcherId_; }
204-215: AlignsetRevertingPayloaddocumentation with watcher-signed access control.NatSpec says “Only callable by owner” but the implementation:
- Accepts any external caller.
- Recovers a
watcherfrom a signature.- Requires
_hasRole(WATCHER_ROLE, watcher).So this is actually “only callable with a valid watcher signature”, not owner‑only. This mismatch can mislead auditors and integrators about who can mark payloads as reverting.
This should either:
- Change the comment to describe watcher-signed semantics, or
- Change implementation to an owner/governance‑only entrypoint if that’s the intended policy.
contracts/protocol/SocketConfig.sol (2)
86-96: ValidatenetworkFeeCollector_when configuring the fee collector
setNetworkFeeCollectoraccepts any address and allows setting a non‑zero EOA or an incorrect contract, which will causecollectNetworkFeecalls inSocketto revert and effectively brick fee collection until governance fixes the config.This should validate:
- Either
networkFeeCollector_ == address(0)(explicitly disabling fee collection), ornetworkFeeCollector_is a deployed contract implementing the expected interface (e.g., via anAddress.isContractcheck plus a lightweight staticcall to a known selector).This avoids silent misconfiguration and preserves the “disable by zero address” behavior you documented.
#!/bin/bash # Quick check: locate INetworkFeeCollector and see expected selectors for a staticcall check rg -n "interface INetworkFeeCollector" -S .
69-84: Guard switchboard status changes and plug connections against non‑existent IDs
disableSwitchboard,enableSwitchboard, andconnectonly consultswitchboardStatus[switchboardId_]and allow updates/connections even whenswitchboardAddresses[switchboardId_] == address(0). Governance canenableSwitchboardon an unregistered ID, after which plugs canconnectto that ID andupdatePlugConfigwill callISwitchboard(address(0)), reverting with a generic “call to non‑contract”.This should explicitly validate the ID is actually registered before changing status or allowing connections:
- In
disableSwitchboardandenableSwitchboard, requireswitchboardAddresses[switchboardId_] != address(0)and revert withInvalidSwitchboard()otherwise.- In
connect, after the existing status check, loadaddress switchboard = switchboardAddresses[switchboardId_];and requireswitchboard != address(0)before using it forupdatePlugConfigand emitting events.This makes misconfigured or unregistered IDs fail fast with a clear error instead of surfacing later as low‑signal reverts.
Also applies to: 106-117
contracts/protocol/Socket.sol (1)
178-191: ImportSafeTransferLibto fix compile error in_handleFailedExecution
_handleFailedExecutioncallsSafeTransferLib.safeTransferETH, but this file does not importSafeTransferLib. Without an import, this will not compile (Identifier not found or not unique: "SafeTransferLib").This should add the correct import at the top of the file:
import "./SocketUtils.sol"; import {WRITE} from "../utils/common/Constants.sol"; -import "../utils/common/Errors.sol"; -using LibCall for address; +import "../utils/common/Errors.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +using LibCall for address;This restores a resolvable reference for
SafeTransferLib.safeTransferETHin the failure refund path.#!/bin/bash # Confirm SafeTransferLib path and all call sites rg -n "SafeTransferLib" -S contractsAlso applies to: 4-7
🧹 Nitpick comments (8)
contracts/protocol/base/MessagePlugBase.sol (2)
19-33: Fail fast ifswitchboardAddressesreturns address(0).Constructor validates
switchboardId_ != 0but does not guard againstsocket__.switchboardAddresses(switchboardId_)returningaddress(0)for an unconfigured ID. This should revert before callingconnectto avoid silently wiring the plug to an invalid switchboard.Example fix:
_setSocket(socket_); switchboardId = switchboardId_; - switchboard = socket__.switchboardAddresses(switchboardId_); - // Connect with empty config (siblings registered separately) - socket__.connect(switchboardId_, ""); + switchboard = socket__.switchboardAddresses(switchboardId_); + if (switchboard == address(0)) revert InvalidSwitchboardId(); + // Connect with empty config (siblings registered separately) + socket__.connect(switchboardId_, "");
47-58: Small gas win in_registerSiblingsloop.Length check is good. The loop currently re-reads
chainSlugs_.lengtheach iteration and usesi++. This should cache the length and use pre‑increment to shave gas in batch registrations.Example:
- function _registerSiblings( - uint32[] memory chainSlugs_, - address[] memory siblingPlugs_ - ) internal { - if (chainSlugs_.length != siblingPlugs_.length) revert ArrayLengthMismatch(); - for (uint256 i = 0; i < chainSlugs_.length; i++) { - _registerSibling(chainSlugs_[i], siblingPlugs_[i]); - } - } + function _registerSiblings( + uint32[] memory chainSlugs_, + address[] memory siblingPlugs_ + ) internal { + uint256 length = chainSlugs_.length; + if (length != siblingPlugs_.length) revert ArrayLengthMismatch(); + for (uint256 i; i < length; ++i) { + _registerSibling(chainSlugs_[i], siblingPlugs_[i]); + } + }auditor-docs/AUDIT_FOCUS_AREAS.md (1)
311-320: Replace bolded function names with proper headings.Lines like
**MessageSwitchboard.attest()**and**SwitchboardBase.getTransmitter()**are flagged (MD036) as “emphasis used instead of a heading”. This should use heading syntax (for example### MessageSwitchboard.attest()/### SwitchboardBase.getTransmitter()) so the structure is machine- and human‑navigable and markdownlint‑clean.Also applies to: 333-343
contracts/protocol/switchboard/MessageSwitchboard.sol (1)
413-437: Document/watch threshold semantics forattestandtotalWatchers.
attestmarks a digest valid only whenattestations[digest_] >= totalWatchers[chainSlug]. Combined withgrantWatcherRole/revokeWatcherRolemutatingtotalWatchers, the effective threshold is “all current watchers for that chainSlug”. If governance later revokes a watcher after only a subset of watchers have attested, that digest will remain permanently invalid (no further attestations possible from already-attested watchers).This is a design choice but subtle. This should:
- Be explicitly documented as an N‑of‑N model tied to
totalWatchers[chainSlug], and- Clarify operational guidance (avoid watcher set changes while digests are pending), or
- Be adjusted to a more flexible threshold model if that’s the intent.
contracts/protocol/switchboard/EVMxSwitchboard.sol (1)
69-71: Remove or emitEvmxConfigSetevent.
EvmxConfigSet(uint32 evmxChainSlug, uint32 evmxWatcherId)is declared but never emitted. Leaving unused public events in production contracts adds noise without value.This should either:
- Emit
EvmxConfigSetin the constructor (or in any future config‑setting function), or- Remove the event if configuration is strictly immutable.
auditor-docs/TESTING_COVERAGE.md (1)
13-22: Annotate fenced code blocks with languages for better tooling support.Many fenced blocks (directory tree, Solidity snippets, pseudo‑code) use bare ``` fences, triggering MD040 and losing syntax highlighting. Examples include:
- Test directory layout (lines 13–22),
- Reentrancy attack test skeletons and other test templates (around lines 185+).
This should tag each block with an appropriate language:
- Use
```plaintextor```textfor directory trees, ASCII diagrams, and pseudo‑output.- Use
```solidityfor Solidity snippets.Also applies to: 185-205
auditor-docs/SETUP_GUIDE.md (1)
52-68: Fix markdown lint issues: add code block languages and avoid bare URLsLint is flagging a few small issues that are easy to fix and improve docs quality:
- Lines 52–68, 259–262, 584–588, 592–595: fenced blocks use plain
without a language. This should use something liketext for directory trees / addresses andbash /toml where appropriate.- Lines 617–624: bare URLs are used. This should wrap them in links, e.g.
[Solidity Docs](https://docs.soliditylang.org/), to satisfy MD034 and improve readability.These changes keep the guide markdown‑lint clean without altering semantics.
Also applies to: 259-262, 584-588, 592-595, 617-624
auditor-docs/SECURITY_MODEL.md (1)
209-216: Clean up markdown code fences and nonce notationThere are a couple of markdown issues flagged by tooling:
- The invariant blocks and logical formulas (lines ~209, 221, 233, 245, 256, 267, 279, 289, 299, 310) use plain
with no language. This should use something liketext to satisfy MD040 and make it clear these are pseudo‑math/logic, not Solidity.- In the “Nonce Management” table, entries like
usedNonces[watcher][nonce]/usedNonces[feeUpdater][nonce]are parsed as reference links with labelnonce, triggering MD052. This should wrap them in backticks (e.g.`usedNonces[watcher][nonce]`) so they render as code and not links.This keeps the security model document lint‑clean and avoids confusing rendering.
Also applies to: 221-223, 233-235, 245-247, 256-259, 267-271, 279-281, 289-291, 299-302, 310-313, 397-402
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
.prettierignore(1 hunks)auditor-docs/AUDIT_FOCUS_AREAS.md(1 hunks)auditor-docs/AUDIT_PREP_SUMMARY.md(1 hunks)auditor-docs/CONTRACTS_REFERENCE.md(1 hunks)auditor-docs/FAQ.md(1 hunks)auditor-docs/MESSAGE_FLOW.md(1 hunks)auditor-docs/README.md(1 hunks)auditor-docs/SECURITY_MODEL.md(1 hunks)auditor-docs/SETUP_GUIDE.md(1 hunks)auditor-docs/SYSTEM_OVERVIEW.md(1 hunks)auditor-docs/TESTING_COVERAGE.md(1 hunks)contracts/evmx/base/AppGatewayBase.sol(6 hunks)contracts/evmx/helpers/ForwarderSolana.sol(1 hunks)contracts/protocol/Socket.sol(1 hunks)contracts/protocol/SocketConfig.sol(1 hunks)contracts/protocol/SocketUtils.sol(3 hunks)contracts/protocol/base/MessagePlugBase.sol(1 hunks)contracts/protocol/interfaces/ISocket.sol(1 hunks)contracts/protocol/switchboard/EVMxSwitchboard.sol(1 hunks)contracts/protocol/switchboard/MessageSwitchboard.sol(3 hunks)contracts/protocol/switchboard/SwitchboardBase.sol(2 hunks)contracts/utils/common/Constants.sol(1 hunks)contracts/utils/common/Errors.sol(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- contracts/utils/common/Constants.sol
- .prettierignore
🧰 Additional context used
🪛 LanguageTool
auditor-docs/SECURITY_MODEL.md
[style] ~104-~104: To form a complete sentence, be sure to include a subject.
Context: ...e (Fully adversarial) Behavior: - May be malicious or buggy - Can attempt ree...
(MISSING_IT_THERE)
[style] ~149-~149: This phrase is redundant (‘OS’ stands for ‘operating system’). Use simply “DOS”.
Context: ...or**: - May underpay fees - May try to DOS system with spam - May attempt double-spending...
(ACRONYM_TAUTOLOGY)
[grammar] ~365-~365: Use a hyphen to join words.
Context: ... 4. State Modification Points #### High Impact State Changes - `executionStatus...
(QB_NEW_EN_HYPHEN)
[grammar] ~515-~515: Use a hyphen to join words.
Context: ...ds --- ## Threat Model Summary ### In Scope Threats - ✓ Malicious plugs attem...
(QB_NEW_EN_HYPHEN)
auditor-docs/AUDIT_PREP_SUMMARY.md
[uncategorized] ~124-~124: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...g, not race condition; low probability, low impact - Verdict: Acceptable **9. Cross-C...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~129-~129: Ensure spelling is correct
Context: ...y** - Reason: CEI pattern + unique payloadIds per call - Verdict: Safe by design ...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
auditor-docs/CONTRACTS_REFERENCE.md
[grammar] ~207-~207: Use a hyphen to join words.
Context: ...mpler attestation model - App gateway ID based routing vs. sibling plug mapping ...
(QB_NEW_EN_HYPHEN)
auditor-docs/FAQ.md
[uncategorized] ~225-~225: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... - Source validation in switchboards Worst Case Scenario: Governance could DOS the sy...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[style] ~536-~536: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...the source plug can increase fees - Can only increase, not decrease - Native fees: A...
(ADVERB_REPETITION_PREMIUM)
[style] ~635-~635: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...ource plug can increase fees 2. Refunds only to specified refundAddress 3. Refunds o...
(ADVERB_REPETITION_PREMIUM)
[style] ~636-~636: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...y to specified refundAddress 3. Refunds only when watcher-approved 4. Refunds only p...
(ADVERB_REPETITION_PREMIUM)
[style] ~637-~637: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...s only when watcher-approved 4. Refunds only possible once 5. Fees in successful exe...
(ADVERB_REPETITION_PREMIUM)
🪛 markdownlint-cli2 (0.18.1)
auditor-docs/SECURITY_MODEL.md
209-209: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
221-221: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
233-233: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
245-245: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
256-256: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
267-267: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
279-279: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
289-289: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
299-299: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
310-310: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
399-399: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
400-400: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
401-401: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
auditor-docs/AUDIT_FOCUS_AREAS.md
311-311: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
335-335: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
auditor-docs/README.md
209-209: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
221-221: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
233-233: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
245-245: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
256-256: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
267-267: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
279-279: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
289-289: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
299-299: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
310-310: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
399-399: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
400-400: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
401-401: Reference links and images should use a label that is defined
Missing link or image reference definition: "nonce"
(MD052, reference-links-images)
auditor-docs/SETUP_GUIDE.md
52-52: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
259-259: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
584-584: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
592-592: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
617-617: Bare URL used
(MD034, no-bare-urls)
618-618: Bare URL used
(MD034, no-bare-urls)
619-619: Bare URL used
(MD034, no-bare-urls)
623-623: Bare URL used
(MD034, no-bare-urls)
624-624: Bare URL used
(MD034, no-bare-urls)
auditor-docs/TESTING_COVERAGE.md
13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
36-36: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
51-51: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
68-68: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
89-89: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
151-151: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
159-159: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
185-185: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
193-193: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
199-199: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
205-205: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
213-213: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
220-220: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
228-228: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
238-238: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
246-246: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
259-259: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
274-274: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
279-279: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
293-293: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
301-301: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
310-310: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
327-327: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
339-339: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
358-358: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
366-366: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
380-380: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
390-390: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
398-398: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
413-413: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
425-425: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
433-433: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
443-443: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
458-458: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
470-470: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
479-479: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
498-498: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
518-518: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
537-537: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
547-547: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
558-558: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
576-576: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
590-590: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
599-599: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
605-605: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
643-643: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
650-650: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
656-656: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
663-663: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
auditor-docs/SYSTEM_OVERVIEW.md
30-30: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
36-36: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
42-42: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
48-48: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
87-87: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
92-92: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
97-97: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
102-102: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
107-107: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
112-112: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
117-117: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
122-122: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
127-127: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
132-132: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
203-203: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
209-209: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
216-216: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
222-222: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
228-228: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
257-257: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
263-263: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
269-269: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
275-275: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
281-281: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
auditor-docs/CONTRACTS_REFERENCE.md
250-250: Reference links and images should use a label that is defined
Missing link or image reference definition: "reserved: 64"
(MD052, reference-links-images)
281-281: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
295-295: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
305-305: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
auditor-docs/FAQ.md
7-7: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
14-14: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
20-20: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
26-26: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
33-33: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
40-40: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
352-352: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
362-362: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
445-445: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
499-499: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
508-508: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
516-516: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
937-937: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
949-949: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
962-962: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
981-981: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
990-990: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
997-997: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🔇 Additional comments (11)
contracts/protocol/SocketUtils.sol (4)
42-50: Constructor validation and immutable setup look correct.Chain slug, owner, and gasLimitBuffer are validated up front and stored as immutables. This should give predictable behavior and prevent misconfigured deployments.
64-94: Digest construction is deterministic and collision‑resistant.Packing fixed fields first and then appending length‑prefixed payload/source/extraData with
abi.encodePackedshould give a stable, unambiguous digest and block the usual concatenation‑collision cases. This is the right shape for attestation integrity.
96-117: Off‑chain simulation gating is tight.Restricting
simulate()via the OFF_CHAIN_CALLER sentinel and usingtryCallwithmaxCopyBytesshould keep this strictly for eth_call‑style simulation and prevent abuse as an on‑chain execution helper. The loop and result wiring are straightforward.
119-167: Plug/switchboard verification and fee top‑ups are wired correctly.
_verifyPlugSwitchboardand_verifyPayloadIdenforce that plugs are connected to a registered switchboard and that payloadIds match the localchainSlugand expected switchboard address beforeincreaseFeesForPayloadforwards funds. This should preserve routing and replay invariants around fee increases.contracts/evmx/helpers/ForwarderSolana.sol (2)
32-51: Solana‑only initialization guard is correct.Restricting
initializeto known Solana slugs and wiringchainSlugvia storage once, while leaving on‑chain address resolution to the resolver, should prevent mis‑deployment of this forwarder on non‑Solana chains.
64-91: Caller authenticity for Solana forwarding is now enforced via msg.sender.
callSolanausesmsg.senderas the app gateway and requiresisAsyncModifierSeton that gateway before building and queuing aRawPayload. This fixes the earlier spoofing risk by no longer trusting a user‑supplied gateway address and tying forwarding to the real caller’s async flow.contracts/evmx/base/AppGatewayBase.sol (2)
197-204: Override clearing keeps persistent fields while resetting per‑call overrides.
_clearOverridespreservesswitchboardTypeandmaxFeeswhile resetting other override fields and settingconsumeFromto this contract, which is a good separation between long‑lived config and per‑call overrides. This should keep state predictable across async flows.
80-88: RawPayload transaction field remains unpopulated in _setSchedule despite Watcher uncertainty.The concern is valid.
_setSchedulecreates aRawPayloadwith zeroedtransactionfields while all other callers (GasAccountManager,Forwarder,ForwarderSolana) explicitly populate it. More critically,Watcher.addPayloadDatacontains a TODO comment specifically asking "what happens in read and schedule?" (Watcher.sol:69), indicating the watcher's schedule handling is unresolved.Either populate
transactionin_setScheduleto match the pattern elsewhere, or explicitly confirm with the watcher maintainer that SCHEDULE payloads intentionally omit transaction data and resolve the TODO.contracts/protocol/switchboard/SwitchboardBase.sol (2)
40-49: Constructor invariants and registration flow are sound.Validating
chainSlug_,socket_, andowner_before setting immutables and invokingsocket__.registerSwitchboard()should guarantee each switchboard is bound to a real socket instance with a non‑zero owner and stableswitchboardId. Coupled withonlySocket, this keeps the call surface constrained.
61-88: Transmitter digest and signer recovery match the documented security model.
getTransmitterderives the digest askeccak256(abi.encodePacked(address(socket__), payloadId_))and_recoverSignerwraps it with the Ethereum Signed Message prefix before ECDSA recovery. This should prevent cross‑contract replay of transmitter signatures and aligns with the signature format described in the audit docs.contracts/protocol/interfaces/ISocket.sol (1)
14-151: ISocket surface matches current Socket/SocketConfig usageEvent and function signatures (execution, plug connect/disconnect, switchboard registration, payload send, and fee top‑up) line up with the implementations shown in
Socket.solandSocketConfig.sol. Documentation is clear enough for auditors to follow the payload‑first model. No changes needed here.
| **Prepared**: [Date] | ||
| **Protocol Version**: [Version] | ||
| **Pre-Audit Review**: Complete ✅ |
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.
Fill in [Date] and [Version] placeholders in audit summary metadata.
The footer still uses placeholders:
**Prepared**: [Date]**Protocol Version**: [Version]
This should be replaced with actual preparation date and protocol version before auditors rely on this document; otherwise the summary can be misinterpreted as stale or incomplete.
🤖 Prompt for AI Agents
In auditor-docs/AUDIT_PREP_SUMMARY.md around lines 364 to 366, the footer still
contains placeholders "**Prepared**: [Date]" and "**Protocol Version**:
[Version]"; replace these with the actual preparation date (e.g., YYYY-MM-DD or
project convention) and the current protocol version string pulled from the
repository/release notes (or the value provided in the audit metadata), ensuring
the entries are accurate and formatted consistently with the rest of the
document.
| - `createPayloadId()`: Encodes components into bytes32 | ||
| - Takes: sourceChainSlug, sourceId, verificationChainSlug, verificationId, pointer | ||
| - Bit layout: [Source: 64][Verification: 64][Pointer: 64][Reserved: 64] | ||
| - `decodePayloadId()`: Extracts all components from bytes32 | ||
| - `getVerificationInfo()`: Extracts verification chain and ID | ||
| - `getSourceInfo()`: Extracts source chain and ID | ||
|
|
||
| **Usage**: Imported and used by Socket and Switchboards for payload ID management. | ||
|
|
||
| --- | ||
|
|
||
| ### 8. OverrideParamsLib.sol | ||
|
|
||
| **Purpose**: Builder pattern library for constructing OverrideParams structs. | ||
|
|
||
| **No State Variables** (all pure functions) | ||
|
|
||
| **Functions**: | ||
|
|
||
| - `clear()`: Creates new OverrideParams with defaults | ||
| - `setRead()`, `setParallel()`, `setWriteFinality()`: Set flags | ||
| - `setGasLimit()`, `setValue()`, `setMaxFees()`: Set numeric values | ||
| - `setReadAtBlock()`, `setDelay()`: Set timing parameters | ||
| - `setConsumeFrom()`, `setSwitchboardType()`: Set addresses/identifiers | ||
|
|
||
| **Usage**: Used by plugs to construct override parameters for payload requests. | ||
|
|
||
| --- | ||
|
|
||
| ## Contract Interactions | ||
|
|
||
| ### Execution Flow (Inbound) | ||
|
|
||
| ``` | ||
| Transmitter → Socket.execute() | ||
| ├─> SocketUtils._verifyPlugSwitchboard() | ||
| ├─> SocketUtils._verifyPayloadId() | ||
| ├─> Socket._verify() | ||
| │ └─> Switchboard.getTransmitter() | ||
| │ └─> Switchboard.allowPayload() | ||
| └─> Socket._execute() | ||
| └─> Plug.call{value, gas}(payload) | ||
| └─> NetworkFeeCollector.collectNetworkFee() | ||
| ``` | ||
|
|
||
| ### Sending Flow (Outbound) | ||
|
|
||
| ``` | ||
| Plug → Socket.sendPayload() | ||
| ├─> SocketUtils._verifyPlugSwitchboard() | ||
| ├─> Plug.overrides() | ||
| └─> Switchboard.processPayload() | ||
| └─> emit PayloadRequested | ||
| ``` | ||
|
|
||
| ### Registration Flow | ||
|
|
||
| ``` | ||
| Switchboard → Socket.registerSwitchboard() | ||
| └─> Assign ID, set status REGISTERED | ||
| Plug → Socket.connect(switchboardId, config) | ||
| └─> Switchboard.updatePlugConfig(plug, config) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Key Data Structures | ||
|
|
||
| ### ExecutionParams | ||
|
|
||
| ```solidity | ||
| struct ExecutionParams { | ||
| bytes4 callType; // WRITE, READ, or SCHEDULE | ||
| uint256 deadline; // Execution deadline timestamp | ||
| uint256 gasLimit; // Gas limit for execution | ||
| address target; // Target plug address | ||
| uint256 value; // Native value to send | ||
| bytes32 payloadId; // Unique payload identifier | ||
| bytes32 prevBatchDigestHash; // For batch processing | ||
| bytes source; // Encoded source info | ||
| bytes payload; // Call data | ||
| bytes extraData; // Additional data | ||
| } | ||
| ``` | ||
|
|
||
| ### TransmissionParams | ||
|
|
||
| ```solidity | ||
| struct TransmissionParams { | ||
| uint256 socketFees; // Fees for Socket/transmitter | ||
| address refundAddress; // Where to refund on failure | ||
| bytes extraData; // Additional parameters | ||
| bytes transmitterProof; // Transmitter signature | ||
| } | ||
| ``` | ||
|
|
||
| ### DigestParams | ||
|
|
||
| ```solidity | ||
| struct DigestParams { | ||
| bytes32 socket; // Destination socket address | ||
| bytes32 transmitter; // Transmitter address | ||
| bytes32 payloadId; // Unique identifier | ||
| uint256 deadline; // Execution deadline | ||
| bytes4 callType; // Call type | ||
| uint256 gasLimit; // Gas limit | ||
| uint256 value; // Native value | ||
| bytes32 target; // Target address | ||
| bytes32 prevBatchDigestHash; | ||
| bytes payload; // Payload data | ||
| bytes source; // Source information | ||
| bytes extraData; // Extra data | ||
| } |
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.
Clean up markdown issues and align ExecutionParams.gasLimit type across docs.
-
The “Bit layout” line (
[Source: 64][Verification: 64][Pointer: 64][Reserved: 64]) is being parsed as undefined reference links (MD052). This should be escaped or wrapped in code, for example:Bit layout: `[Source: 64][Verification: 64][Pointer: 64][Reserved: 64]`
-
The ASCII diagrams under “Execution Flow (Inbound)”, “Sending Flow (Outbound)”, and “Registration Flow” use fenced code blocks without a language (MD040). This should specify a language like
textto keep markdownlint happy and signal intent:Transmitter → Socket.execute() ... -
Here
ExecutionParamsdocumentsuint256 gasLimit, while AUDIT_FOCUS_AREAS.md’s gas handling section describesgasLimitasuint64. This should be made consistent with the actualExecutionParamsdefinition inStructs.solso auditors are not misled about range/overflow behaviour.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
250-250: Reference links and images should use a label that is defined
Missing link or image reference definition: "reserved: 64"
(MD052, reference-links-images)
281-281: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
295-295: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
305-305: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In auditor-docs/CONTRACTS_REFERENCE.md around lines 248–361, fix three markdown
and accuracy issues: 1) Prevent the "Bit layout" line from being parsed as
reference links by wrapping it in an inline code span or a code block (e.g.,
replace [Source: 64][Verification: 64][Pointer: 64][Reserved: 64] with a
backticked or fenced representation). 2) Add a language tag (use "text") to the
fenced code blocks for the ASCII diagrams under Execution Flow (Inbound),
Sending Flow (Outbound), and Registration Flow to satisfy MD040. 3) Reconcile
the documented type of ExecutionParams.gasLimit with the actual type in
Structs.sol (open Structs.sol to confirm whether it is uint64 or uint256) and
update the CONTRACTS_REFERENCE.md entry to match exactly so auditors see the
correct range/overflow behavior.
| ### Execution Status Transitions | ||
|
|
||
| ``` | ||
| NotExecuted → Executed (success path) | ||
| NotExecuted → Reverted (failure path) | ||
| Note: One-way transitions, no re-execution | ||
| ``` |
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.
Execution status diagram does not match current implementation behavior
This section documents NotExecuted → Executed / NotExecuted → Reverted as one‑way transitions with “no re‑execution”, but Socket._validateExecutionStatus currently only blocks when status is Executed, so a Reverted payload can be executed again and the status can transition Reverted → Executed → Reverted. This should be aligned by fixing the code so any non‑zero status is terminal (preferred), and then keeping this diagram as the source of truth, or by updating the diagram if re‑execution after revert is actually desired.
🤖 Prompt for AI Agents
In auditor-docs/MESSAGE_FLOW.md around lines 588 to 595, the documented
execution-status transitions claim one-way, no re-execution (NotExecuted →
Executed / NotExecuted → Reverted) but the implementation
(Socket._validateExecutionStatus) only blocks when status === Executed allowing
Reverted → Executed replays; update the implementation to treat any
non-zero/non-NotExecuted status as terminal by changing the validation to reject
execution when status !== NotExecuted (or status !== 0 depending on
representation), add a unit test asserting that a Reverted status cannot
transition to Executed, and keep this diagram as the source of truth (or, if
re-execution after revert is actually desired, instead update this doc to
reflect the allowed Reverted → Executed → Reverted transitions and add a note
explaining intent).
| **Documentation Version**: 1.0 | ||
| **Last Updated**: [Date] | ||
| **Protocol Version**: [Version] | ||
| **Audit Firm**: [Firm Name] | ||
| **Point of Contact**: [Name/Email] |
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.
Resolve placeholder audit metadata in README footer.
The README footer still contains placeholders:
**Last Updated**: [Date]**Protocol Version**: [Version]**Audit Firm**: [Firm Name]**Point of Contact**: [Name/Email]
This should be filled with real values (or removed) before sharing with auditors, to avoid confusion about who to contact and which protocol version the docs describe.
🤖 Prompt for AI Agents
In auditor-docs/README.md around lines 593 to 597, the README footer contains
placeholder audit metadata ([Date], [Version], [Firm Name], [Name/Email]);
replace each placeholder with the actual values (or remove the fields if not
applicable) so the footer contains concrete "Last Updated: YYYY-MM-DD",
"Protocol Version: X.Y", "Audit Firm: <Firm Name>" and "Point of Contact: Name
<email@domain>" entries; ensure formatting matches surrounding markdown and
commit the change.
| ### 1. Execution Uniqueness | ||
|
|
||
| ``` | ||
| ∀ payloadId: executionStatus[payloadId] ∈ {NotExecuted, Executed, Reverted} | ||
| ``` | ||
|
|
||
| Once set to Executed or Reverted, status cannot change. | ||
|
|
||
| **Consequence**: No payload can be executed twice. | ||
|
|
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.
Execution uniqueness invariant is violated by current Socket implementation
This invariant states that once executionStatus[payloadId] is Executed or Reverted it never changes and “no payload can be executed twice”. In Socket.sol, _validateExecutionStatus only rejects when status is Executed, sets it to Executed before the call, and _handleFailedExecution later overwrites it to Reverted. This allows transitions Executed → Reverted and re‑execution of a Reverted payload. This should be fixed in Socket.sol by (a) treating any non‑zero status as terminal and (b) only setting Executed on successful execution, so this invariant holds as written.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
209-209: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In auditor-docs/SECURITY_MODEL.md around lines 207 to 216, the Socket.sol
implementation violates the execution uniqueness invariant by pre-setting
Executed and later overwriting to Reverted which allows re‑execution; change
_validateExecutionStatus to treat any non‑zero status as terminal (reject if
status != NotExecuted), stop setting executionStatus to Executed before the
external call, set executionStatus = Executed only after the execution completes
successfully, and have _handleFailedExecution set executionStatus = Reverted
only when the prior status is NotExecuted (i.e., ensure writes only transition
NotExecuted→Executed or NotExecuted→Reverted and never overwrite a non‑zero
terminal status).
| /// @notice Registers a sibling plug for a specific destination chain | ||
| /// @param chainSlug_ Chain slug of the destination chain | ||
| /// @param siblingPlug_ Address of the sibling plug on the destination chain | ||
| function registerSibling(uint32 chainSlug_, address siblingPlug_) public { | ||
| /// @dev Updates plug config via socket.connect() with chain slug and sibling plug address | ||
| function _registerSibling(uint32 chainSlug_, address siblingPlug_) internal { | ||
| // Call the switchboard to register the sibling | ||
| IMessageSwitchboard(switchboard).registerSibling(chainSlug_, toBytes32Format(siblingPlug_)); | ||
| } | ||
|
|
||
| function getSocketFees(uint32 chainSlug_) public view returns (uint256) { | ||
| return IMessageSwitchboard(switchboard).getSwitchboardFees(chainSlug_); | ||
| // Using abi.encodePacked to match digest encoding (needed for Solana compatibility) | ||
| socket__.connect(switchboardId, abi.encode(chainSlug_, toBytes32Format(siblingPlug_))); | ||
| } |
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 abi.encode vs abi.encodePacked mismatch for sibling registration.
Line 43 states this should use abi.encodePacked to match the digest encoding (Solana compatibility), but line 44 uses abi.encode. For cross‑chain verification, the exact byte layout is critical. This should either:
- Switch to
abi.encodePackedto match the documented digest, or - Update the comment and any downstream decoders/digests to expect standard ABI encoding.
If the digest expects packed (chainSlug_, siblingPlugId) bytes, the implementation should look like:
- // Using abi.encodePacked to match digest encoding (needed for Solana compatibility)
- socket__.connect(switchboardId, abi.encode(chainSlug_, toBytes32Format(siblingPlug_)));
+ // Using abi.encodePacked to match digest encoding (needed for Solana compatibility)
+ socket__.connect(
+ switchboardId,
+ abi.encodePacked(chainSlug_, toBytes32Format(siblingPlug_))
+ );📝 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 Registers a sibling plug for a specific destination chain | |
| /// @param chainSlug_ Chain slug of the destination chain | |
| /// @param siblingPlug_ Address of the sibling plug on the destination chain | |
| function registerSibling(uint32 chainSlug_, address siblingPlug_) public { | |
| /// @dev Updates plug config via socket.connect() with chain slug and sibling plug address | |
| function _registerSibling(uint32 chainSlug_, address siblingPlug_) internal { | |
| // Call the switchboard to register the sibling | |
| IMessageSwitchboard(switchboard).registerSibling(chainSlug_, toBytes32Format(siblingPlug_)); | |
| } | |
| function getSocketFees(uint32 chainSlug_) public view returns (uint256) { | |
| return IMessageSwitchboard(switchboard).getSwitchboardFees(chainSlug_); | |
| // Using abi.encodePacked to match digest encoding (needed for Solana compatibility) | |
| socket__.connect(switchboardId, abi.encode(chainSlug_, toBytes32Format(siblingPlug_))); | |
| } | |
| /// @notice Registers a sibling plug for a specific destination chain | |
| /// @param chainSlug_ Chain slug of the destination chain | |
| /// @param siblingPlug_ Address of the sibling plug on the destination chain | |
| /// @dev Updates plug config via socket.connect() with chain slug and sibling plug address | |
| function _registerSibling(uint32 chainSlug_, address siblingPlug_) internal { | |
| // Call the switchboard to register the sibling | |
| // Using abi.encodePacked to match digest encoding (needed for Solana compatibility) | |
| socket__.connect( | |
| switchboardId, | |
| abi.encodePacked(chainSlug_, toBytes32Format(siblingPlug_)) | |
| ); | |
| } |
| function execute( | ||
| ExecuteParams calldata executeParams_, | ||
| ExecutionParams memory executionParams_, | ||
| TransmissionParams calldata transmissionParams_ | ||
| ) external payable returns (bool, bytes memory) { | ||
| // check if the deadline has passed | ||
| if (executeParams_.deadline < block.timestamp) revert DeadlinePassed(); | ||
| ) external payable whenNotPaused returns (bool, bytes memory) { |
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
Ensure execute correctly implements ISocket.execute (override and data location)
Socket inherits ISocket via SocketUtils/SocketConfig, but this execute implementation:
function execute(
ExecutionParams memory executionParams_,
TransmissionParams calldata transmissionParams_
) external payable whenNotPaused returns (bool, bytes memory) {has no override specifier and uses ExecutionParams memory while the interface declares ExecutionParams calldata. To avoid the contract remaining abstract or failing to compile, this should:
- Be explicitly marked as
override(oroverride(ISocket)if needed). - Use the same data location as the interface for the first parameter (
calldata) unless there is a strong reason not to, updating_verify/_executesignatures accordingly if you switch.
This should be verified against the compiler to ensure the implementation is correctly wired into the public interface.
🏁 Script executed:
#!/bin/bash
# Show all execute declarations and check override/data location consistency
rg -n "function execute" -S contractsLength of output: 358
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Extract full execute signatures from ISocket.sol
echo "=== ISocket.sol execute signature ==="
sed -n '74,80p' contracts/protocol/interfaces/ISocket.sol
echo ""
echo "=== Socket.sol execute signature ==="
sed -n '46,50p' contracts/protocol/Socket.sol
echo ""
echo "=== Socket.sol inheritance ==="
head -50 contracts/protocol/Socket.sol | grep -E "contract Socket|is "Length of output: 920
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Check SocketUtils and SocketConfig inheritance
echo "=== SocketUtils.sol first 30 lines ==="
head -30 contracts/protocol/SocketUtils.sol | grep -E "contract SocketUtils|is "
echo ""
echo "=== SocketConfig.sol first 30 lines ==="
head -30 contracts/protocol/SocketConfig.sol | grep -E "contract SocketConfig|is "
echo ""
echo "=== Check if ISocket is imported/referenced in Socket.sol ==="
head -20 contracts/protocol/Socket.solLength of output: 1096
Fix Socket.execute signature to match ISocket interface: add override and change data location to calldata
The inheritance chain is verified: Socket → SocketUtils → SocketConfig → ISocket. The implementation in Socket.sol:46-49 has two critical issues:
-
Data location mismatch: ISocket declares
ExecutionParams calldatabut Socket usesExecutionParams memory. In Solidity 0.8.28, parameter data locations are part of the function signature—this breaks interface compliance. -
Missing
overridespecifier: Without it, the Solidity compiler treats this as a new function rather than an override, causing compilation failures.
This should be fixed to:
function execute(
ExecutionParams calldata executionParams_,
TransmissionParams calldata transmissionParams_
) external payable override whenNotPaused returns (bool, bytes memory) {Then update any internal call sites (_verify, _execute, etc.) that depend on executionParams_ being in memory to handle calldata appropriately.
🤖 Prompt for AI Agents
In contracts/protocol/Socket.sol around lines 46-49, the execute function
signature does not match ISocket: change the first parameter to ExecutionParams
calldata, add the override specifier, and keep transmissionParams_ as calldata;
ensure the function remains external payable whenNotPaused and returns (bool,
bytes memory). After changing the signature, update internal call sites and
helper functions that expect ExecutionParams memory (e.g., _verify, _execute) to
accept calldata or explicitly copy executionParams_ to memory where mutation is
required (e.g., by creating a memory variable ExecutionParams memory exec =
executionParams_;), so all uses compile and preserve behavior.
| function _execute( | ||
| bytes32 payloadId_, | ||
| ExecuteParams calldata executeParams_, | ||
| ExecutionParams memory executionParams_, | ||
| TransmissionParams calldata transmissionParams_ | ||
| ) internal returns (bool success, bytes memory returnData) { | ||
| // check if the gas limit is sufficient | ||
| // bump by 5% to account for gas used by current contract execution | ||
| if (gasleft() < (executeParams_.gasLimit * gasLimitBuffer) / 100) revert LowGasLimit(); | ||
| // Gas buffer (105) accounts for ~5% overhead from current contract execution | ||
| if (gasleft() < (executionParams_.gasLimit * gasLimitBuffer) / 100) revert LowGasLimit(); | ||
|
|
||
| // NOTE: external un-trusted call | ||
| // NOTE: external un-trusted call to target plug | ||
| bool exceededMaxCopy; | ||
| (success, exceededMaxCopy, returnData) = executeParams_.target.tryCall( | ||
| executeParams_.value, | ||
| executeParams_.gasLimit, | ||
| (success, exceededMaxCopy, returnData) = executionParams_.target.tryCall( | ||
| executionParams_.value, | ||
| executionParams_.gasLimit, | ||
| maxCopyBytes, | ||
| executeParams_.payload | ||
| executionParams_.payload | ||
| ); | ||
|
|
||
| // Handle execution result | ||
| if (success) { | ||
| emit ExecutionSuccess(payloadId_, exceededMaxCopy, returnData); | ||
|
|
||
| // pay and check fees | ||
| if (address(socketFeeManager) != address(0)) { | ||
| socketFeeManager.payAndCheckFees{value: transmissionParams_.socketFees}( | ||
| executeParams_, | ||
| transmissionParams_ | ||
| ); | ||
| } | ||
| _handleSuccessfulExecution( | ||
| exceededMaxCopy, | ||
| returnData, | ||
| executionParams_, | ||
| transmissionParams_ | ||
| ); | ||
| } else { | ||
| payloadExecuted[payloadId_] = ExecutionStatus.Reverted; | ||
|
|
||
| // refund the fees | ||
| address receiver = transmissionParams_.refundAddress; | ||
| if (receiver == address(0)) receiver = msg.sender; | ||
| SafeTransferLib.forceSafeTransferETH(receiver, msg.value); | ||
| emit ExecutionFailed(payloadId_, exceededMaxCopy, returnData); | ||
| _handleFailedExecution( | ||
| executionParams_.payloadId, | ||
| exceededMaxCopy, | ||
| returnData, | ||
| transmissionParams_.refundAddress | ||
| ); | ||
| } | ||
| return (success, returnData); | ||
| } | ||
|
|
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 execution status handling to enforce single execution per payload
Current status logic does not match the intended “one‑shot” execution semantics:
_validateExecutionStatusonly blocks when status isExecutedand then unconditionally sets it toExecuted._handleFailedExecutionlater overwritesexecutionStatus[payloadId_]toExecutionStatus.Reverted.
This has two problems:
- Status can transition
Executed → Reverted, violating the invariant that onceExecuted/Revertedit never changes. - A payload with status
Revertedcan be executed again because_validateExecutionStatusdoes not treatRevertedas terminal.
This should treat any non‑zero status as terminal, and only set Executed on the success path. One way to do that with the existing enum is:
function _execute(
- ExecutionParams memory executionParams_,
+ ExecutionParams memory executionParams_,
TransmissionParams calldata transmissionParams_
) internal returns (bool success, bytes memory returnData) {
@@
- // Handle execution result
- if (success) {
- _handleSuccessfulExecution(
+ // Handle execution result
+ if (success) {
+ _handleSuccessfulExecution(
exceededMaxCopy,
returnData,
executionParams_,
transmissionParams_
);
} else {
_handleFailedExecution(
executionParams_.payloadId,
exceededMaxCopy,
returnData,
transmissionParams_.refundAddress
);
}
@@
function _handleSuccessfulExecution(
@@
- ) internal {
+ ) internal {
+ // Mark as executed (terminal state)
+ executionStatus[executionParams_.payloadId] = ExecutionStatus.Executed;
emit ExecutionSuccess(executionParams_.payloadId, exceededMaxCopy_, returnData_);
@@
function _handleFailedExecution(
@@
- ) internal {
- executionStatus[payloadId_] = ExecutionStatus.Reverted;
+ ) internal {
+ // Mark as reverted (terminal state)
+ executionStatus[payloadId_] = ExecutionStatus.Reverted;
@@
function _validateExecutionStatus(bytes32 payloadId_) internal {
- if (executionStatus[payloadId_] == ExecutionStatus.Executed)
- revert PayloadAlreadyExecuted();
-
- executionStatus[payloadId_] = ExecutionStatus.Executed;
+ ExecutionStatus status = executionStatus[payloadId_];
+ // Only allow when in the default "not executed" state (enum value 0)
+ if (status != ExecutionStatus(0)) revert PayloadAlreadyExecuted();
}This preserves the existing enum, blocks any second attempt (whether the previous outcome was success or revert), and aligns implementation with the documented execution invariants.
Also applies to: 148-191, 193-203
| function getPlugConfig( | ||
| address plug_, | ||
| bytes memory extraData_ | ||
| ) external view override returns (bytes memory plugConfig_) { | ||
| uint32 chainSlug_ = abi.decode(extraData_, (uint32)); | ||
| plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]); | ||
| } |
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 getPlugConfig encoding with updatePlugConfig expectations.
updatePlugConfig decodes plugConfig_ as (uint32 siblingChainSlug, bytes32 siblingPlug), but getPlugConfig currently returns only abi.encode(siblingPlugs[chainSlug_][plug_]). If any caller follows the natural pattern updatePlugConfig(plug, getPlugConfig(plug, extraData_)), this abi.decode(…, (uint32, bytes32)) will revert because the buffer is just one word, not two.
This should return the same tuple shape that updatePlugConfig expects, using the same chainSlug_ that arrives in extraData_:
function getPlugConfig(
address plug_,
bytes memory extraData_
) external view override returns (bytes memory plugConfig_) {
- uint32 chainSlug_ = abi.decode(extraData_, (uint32));
- plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]);
+ uint32 chainSlug_ = abi.decode(extraData_, (uint32));
+ plugConfig_ = abi.encode(chainSlug_, siblingPlugs[chainSlug_][plug_]);
}🏁 Script executed:
#!/bin/bash
# Find updatePlugConfig implementation
rg -n "updatePlugConfig" --type=sol -A 10 -B 2Length of output: 27
🏁 Script executed:
#!/bin/bash
# Find all getPlugConfig calls
rg -n "getPlugConfig" --type=sol -A 3 -B 3Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for abi.decode calls with plugConfig_ to see the expected tuple
rg -n "abi\.decode.*plugConfig_" --type=sol -A 2 -B 2Length of output: 27
🏁 Script executed:
#!/bin/bash
# Look for test files related to MessageSwitchboard
fd -e "test" -o -e "spec" | grep -i "switchboard\|message"Length of output: 116
🏁 Script executed:
#!/bin/bash
# Find updatePlugConfig implementation - search all files
rg -n "updatePlugConfig" -A 10 -B 2Length of output: 22963
🏁 Script executed:
#!/bin/bash
# Find getPlugConfig calls and implementations
rg -n "getPlugConfig" -A 5 -B 2Length of output: 18149
🏁 Script executed:
#!/bin/bash
# Search for abi.decode patterns with plugConfig_
rg -n "abi\.decode.*plugConfig_" -A 2 -B 2Length of output: 1094
🏁 Script executed:
#!/bin/bash
# Search for the MessageSwitchboard.sol file and check its content around the functions
rg -n "function getPlugConfig|function updatePlugConfig" contracts/protocol/switchboard/MessageSwitchboard.sol -A 8Length of output: 743
🏁 Script executed:
#!/bin/bash
# Look for test files
fd "\.test\." -o "\.spec\." | head -20Length of output: 135
Encoding mismatch confirmed: getPlugConfig must return the tuple shape updatePlugConfig expects.
The issue is verified. updatePlugConfig at line 667 decodes plugConfig_ as (uint32 siblingChainSlug, bytes32 siblingPlug), but getPlugConfig at line 697 returns only abi.encode(siblingPlugs[chainSlug_][plug_]) — a single bytes32 value. This breaks the natural round-trip pattern: calling updatePlugConfig(plug, getPlugConfig(plug, extraData_)) will revert because the buffer contains one word instead of two.
The fix is correct—include chainSlug_ in the encoding:
function getPlugConfig(
address plug_,
bytes memory extraData_
) external view override returns (bytes memory plugConfig_) {
uint32 chainSlug_ = abi.decode(extraData_, (uint32));
- plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]);
+ plugConfig_ = abi.encode(chainSlug_, siblingPlugs[chainSlug_][plug_]);
}📝 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 getPlugConfig( | |
| address plug_, | |
| bytes memory extraData_ | |
| ) external view override returns (bytes memory plugConfig_) { | |
| uint32 chainSlug_ = abi.decode(extraData_, (uint32)); | |
| plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]); | |
| } | |
| function getPlugConfig( | |
| address plug_, | |
| bytes memory extraData_ | |
| ) external view override returns (bytes memory plugConfig_) { | |
| uint32 chainSlug_ = abi.decode(extraData_, (uint32)); | |
| plugConfig_ = abi.encode(chainSlug_, siblingPlugs[chainSlug_][plug_]); | |
| } |
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/MessageSwitchboard.sol around lines 692 to
698, getPlugConfig currently encodes and returns only the siblingPlug bytes32
but updatePlugConfig expects a two-word tuple (uint32 siblingChainSlug, bytes32
siblingPlug); change getPlugConfig to abi.encode(chainSlug_,
siblingPlugs[chainSlug_][plug_]) so the returned buffer matches the tuple shape
that updatePlugConfig decodes.
| /// @notice Error thrown when a payload is already cancelled | ||
| error DeadlineNotPassedForOnChainRevert(); | ||
|
|
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 mismatched documentation for DeadlineNotPassedForOnChainRevert.
The comment says “Error thrown when a payload is already cancelled” but the error name is DeadlineNotPassedForOnChainRevert(), which describes a different condition (deadline not yet passed). This should either:
- Rename the error to match the “already cancelled” semantics, or
- Update the comment to describe the actual revert condition used in code paths.
🤖 Prompt for AI Agents
In contracts/utils/common/Errors.sol around lines 48-50, the NatSpec comment for
the error does not match the error name; either rename the error to reflect
"already cancelled" or update the comment to reflect the actual condition. To
fix, choose one approach: A) Rename the error to something like
PayloadAlreadyCancelled() and update every contract/import that references
DeadlineNotPassedForOnChainRevert to the new name (and run/adjust tests), or B)
keep the current error name and change the comment to accurately describe it
(e.g., "Error thrown when the deadline for on-chain revert has not yet passed"),
then run static checks and tests to ensure no broken references remain.
Summary by CodeRabbit
New Features
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.