Skip to content

YNU-789: refactor(ChannelHub): reduce code size, apply style#559

Merged
philanton merged 10 commits into
mainfrom
refactor/channel-hub
Feb 17, 2026
Merged

YNU-789: refactor(ChannelHub): reduce code size, apply style#559
philanton merged 10 commits into
mainfrom
refactor/channel-hub

Conversation

@nksazonov
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Node validator registration API; contract exposes a version constant and a default signature validator.
  • Bug Fixes / Reliability

    • Replaced vague revert strings with specific error types for clearer failure diagnostics.
    • Strengthened state and fund validation to reduce invalid transitions.
  • Chores

    • Added remappings for dependency resolution and updated test import paths.
    • Increased compiler optimizer settings for improved builds.

@nksazonov nksazonov requested a review from a team as a code owner February 16, 2026 20:54
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 2026

Warning

Rate limit exceeded

@nksazonov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR updates Foundry config and remappings, standardizes import paths, adds many explicit custom error types replacing string reverts across core engines, introduces node validator registration and a DEFAULT_SIG_VALIDATOR in ChannelHub, refactors validation into phased checks and delta-based effect calculations, and updates tests to the new imports and selector-based reverts.

Changes

Cohort / File(s) Summary
Configuration
contracts/foundry.toml, contracts/remappings.txt
Increased optimizer_runs for ChannelHub from 50 → 20,000; added remappings for @openzeppelin/contracts and forge-std.
Core — ChannelHub
contracts/src/ChannelHub.sol
Added VERSION and DEFAULT_SIG_VALIDATOR; new registerNodeValidator API; renamed/added public errors (e.g., IncorrectAmount, ValidatorNotApproved, EmptySignature, IncorrectSignature, InsufficientBalance, IncorrectStateIntent, IncorrectChannelStatus, etc.); added internal helpers (_isHomeChain, _pullFunds, _pushFunds, _processHomeChainEscrow*); migrated import paths to @openzeppelin.
Core — ChannelEngine
contracts/src/ChannelEngine.sol
Replaced string reverts with many new error types; reorganized validation into Phase 1 (universal), Phase 2 (intent-specific) and Phase 3 (invariants); switched deposit/withdraw helpers to delta-based signatures; updated many internal helpers and error signaling.
Core — Escrow Engines
contracts/src/EscrowDepositEngine.sol, contracts/src/EscrowWithdrawalEngine.sol
Introduced many custom errors, replaced string-based require messages, preserved public validate interfaces while moving to error-driven guarded flows and strengthened fund/allocation invariants.
Utilities & Math
contracts/src/Utils.sol, contracts/src/WadMath.sol, contracts/src/sigValidators/EcdsaSignatureUtils.sol
Updated import paths to canonical remappings; added decimal-related errors (DecimalsExceedMaxPrecision, DecimalsMismatch, FailedToFetchDecimals); small API renames (pack, toSigningData, isEmpty overload changes).
Tests & Mocks
contracts/test/*, contracts/test/sigValidators/*, contracts/test/mocks/MockERC20.sol
Switched test imports to remapped paths (forge-std, @openzeppelin/contracts); updated revert expectations to use selector-based checks (WadMath tests); no behavioral test logic changes.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Caller as External Caller
participant Hub as ChannelHub
participant ValidatorReg as ValidatorRegistry (internal)
participant SigVal as ISignatureValidator
Caller->>Hub: registerNodeValidator(node, id, validator, signature)
Hub->>ValidatorReg: _validateRegistration(signature)
ValidatorReg-->>Hub: approval / validatorId
Hub->>SigVal: check DEFAULT_SIG_VALIDATOR (fallback)
Hub-->>ValidatorReg: store mapping node -> (validatorId, validator)
Hub-->>Caller: emit NodeValidatorRegistered

mermaid
sequenceDiagram
participant Offchain as Offchain Actor
participant Hub as ChannelHub
participant Engine as ChannelEngine
participant Vault as ERC20/Vault
Offchain->>Hub: submit state transition + signatures
Hub->>Engine: validateTransition(ctx, candidate)
Engine->>Engine: Phase1: universal checks (errors)
Engine->>Engine: Phase2: intent-specific calc (deltas → effects)
Engine->>Engine: Phase3: invariants (funds/alloc checks)
Engine-->>Hub: TransitionEffects
Hub->>Vault: _pullFunds/_pushFunds based on effects
Vault-->>Hub: transfer confirmations
Hub-->>Offchain: transition applied / events

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ready

Suggested reviewers

  • philanton
  • dimast-x

Poem

🐰 I hopped through imports, neat and spry,

Errors now chirp instead of cry,
Validators line up at the gate,
Deltas tally, ledgers sate,
A quiet thump—contracts sigh.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title references ChannelHub refactoring for code size and style, but the PR encompasses extensive changes across 12+ files including comprehensive error handling refactors, import path updates, and significant validation logic changes in ChannelEngine, EscrowDepositEngine, EscrowWithdrawalEngine, and Utils—well beyond just ChannelHub code size reduction. Update the title to reflect the broader scope: consider 'refactor: standardize error handling and update import paths across channel/escrow engines' or similar to accurately represent the multi-file, multi-concern nature of changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/channel-hub

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @nksazonov, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on a significant refactoring of the core smart contracts, primarily ChannelHub, to enhance efficiency and maintainability. The changes aim to reduce contract bytecode size, improve error handling through custom errors, and streamline dependency imports. These updates contribute to a more robust and gas-optimized system, making future development and auditing more straightforward.

Highlights

  • Code Size Optimization: Increased the Solidity optimizer runs for the ChannelHub contract from 50 to 20,000 to reduce its deployed bytecode size, addressing potential contract size limits.
  • Custom Error Implementation: Replaced numerous string-based require and revert messages with custom error types across ChannelEngine.sol, ChannelHub.sol, EscrowDepositEngine.sol, EscrowWithdrawalEngine.sol, Utils.sol, and WadMath.sol. This improves gas efficiency and provides more structured error handling.
  • Dependency Import Refactoring: Introduced a remappings.txt file and updated all OpenZeppelin and Forge-Std import paths to use aliases (e.g., @openzeppelin/contracts/ instead of lib/openzeppelin-contracts/contracts/), streamlining dependency management.
  • ChannelHub Logic Refinement: Refactored the ChannelHub contract by moving constants, optimizing the getOpenChannels function for better performance, and extracting common escrow processing logic into new internal helper functions (_processHomeChainEscrowInitiate, _processHomeChainEscrowFinalize) to reduce redundancy and improve readability.
  • Test File Updates: Updated test files to align with the new import remappings and adjusted WadMath.t.sol to expect custom error selectors, reflecting the changes in error handling.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • contracts/foundry.toml
    • Updated optimizer_runs for the channelhub profile and src/ChannelHub.sol to 20,000.
  • contracts/remappings.txt
    • Added new file to define import remappings for OpenZeppelin and Forge-Std libraries.
  • contracts/src/ChannelEngine.sol
    • Updated OpenZeppelin import paths to use remappings.
    • Added 22 custom error definitions.
    • Replaced numerous string-based require messages with custom errors.
    • Modified _calculateDepositEffects and _calculateWithdrawEffects function signatures to remove the candidate parameter.
    • Updated calls to _calculateDepositEffects and _calculateWithdrawEffects to match new signatures.
    • Replaced generic revert statements with specific custom error reverts.
  • contracts/src/ChannelHub.sol
    • Updated OpenZeppelin import paths to use remappings.
    • Moved VERSION and DEFAULT_SIG_VALIDATOR constants from global scope to contract storage.
    • Optimized getOpenChannels function using a single pass filter and assembly for array resizing.
    • Replaced string error messages with custom errors like IncorrectAmount, InsufficientBalance, IncorrectStateIntent, IncorrectChannelStatus, ChallengerVersionTooLow, OnlyNonHomeEscrowsCanBeChallenged, EmptySignature, IncorrectSignature, and ValidatorNotApproved.
    • Extracted common home chain escrow initiation logic into _processHomeChainEscrowInitiate.
    • Extracted common home chain escrow finalization logic into _processHomeChainEscrowFinalize.
    • Moved _isHomeChain, _pullFunds, and _pushFunds internal functions to the end of the contract and updated their error messages to custom errors.
    • Removed ChannelDoesNotExist error definition.
  • contracts/src/EscrowDepositEngine.sol
    • Updated OpenZeppelin import paths to use remappings.
    • Added 18 custom error definitions.
    • Replaced numerous string-based require messages with custom errors.
    • Replaced generic revert statements with specific custom error reverts.
  • contracts/src/EscrowWithdrawalEngine.sol
    • Updated OpenZeppelin import paths to use remappings.
    • Added 20 custom error definitions.
    • Replaced numerous string-based require messages with custom errors.
    • Replaced generic revert statements with specific custom error reverts.
  • contracts/src/Utils.sol
    • Updated OpenZeppelin import paths to use remappings.
    • Added 3 custom error definitions for decimal validation.
    • Replaced string-based revert messages with custom errors in validateTokenDecimals.
  • contracts/src/WadMath.sol
    • Added DecimalsExceedMaxPrecision custom error.
    • Replaced string-based revert messages with the custom error in toWad functions.
  • contracts/src/sigValidators/EcdsaSignatureUtils.sol
    • Updated OpenZeppelin import paths to use remappings.
  • contracts/test/ChannelHub_Base.t.sol
    • Updated Forge-Std import paths to use remappings.
  • contracts/test/TestUtils.sol
    • Updated Forge-Std and OpenZeppelin import paths to use remappings.
  • contracts/test/Utils.t.sol
    • Updated Forge-Std import paths to use remappings.
  • contracts/test/WadMath.t.sol
    • Updated Forge-Std import paths to use remappings.
    • Modified test cases to expect WadMath.DecimalsExceedMaxPrecision.selector instead of string error messages.
  • contracts/test/mocks/MockERC20.sol
    • Updated OpenZeppelin import paths to use remappings.
  • contracts/test/sigValidators/ECDSAValidator.t.sol
    • Updated Forge-Std import paths to use remappings.
  • contracts/test/sigValidators/EcdsaSignatureUtils.t.sol
    • Updated Forge-Std import paths to use remappings.
  • contracts/test/sigValidators/SessionKeyValidator.t.sol
    • Updated Forge-Std import paths to use remappings.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed refactoring focused on reducing contract code size, improving gas efficiency, and applying consistent styling. The primary changes include replacing require statements with custom errors, optimizing functions like getOpenChannels, and extracting duplicated logic into helper functions in ChannelHub.sol. These changes greatly improve the codebase's maintainability and performance. The PR also introduces minor but reasonable logic changes to allow challenging and closing channels in the MIGRATING_IN state. I've identified a couple of minor instances where incorrect custom errors were used and have left specific comments for those.

Comment thread contracts/src/ChannelEngine.sol Outdated
Comment thread contracts/src/EscrowWithdrawalEngine.sol Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@contracts/foundry.toml`:
- Around line 12-19: The comment claiming "prevent code size overflow" is now
stale given optimizer_runs = 20_000; update the comments around
additional_compiler_profiles and compilation_restrictions (the ChannelHub
profile and the ChannelHub.sol entry) to explain that ChannelHub is
intentionally compiled with high optimizer_runs to favor runtime gas efficiency
because the contract size was refactored to fit deployment limits, rather than
saying it prevents code size overflow; keep the references to name =
"channelhub" and optimizer_runs = 20_000 and adjust both comment lines to
reflect runtime optimization intent.

In `@contracts/src/ChannelEngine.sol`:
- Around line 505-508: The two require checks in
_calculateFinalizeMigrationEffects use the wrong error types: replace
IncorrectUserNetFlow and IncorrectNodeNetFlow with the consistent delta-specific
errors IncorrectUserNetFlowDelta and IncorrectNodeNetFlowDelta so the revert
signals match other net-flow delta validations; update the two require(...)
calls to use IncorrectUserNetFlowDelta for userNfDelta and
IncorrectNodeNetFlowDelta for nodeNfDelta.
- Around line 102-103: The ChannelEngine acceptance check currently allows any
candidate.version when ctx.prevState.version == 0; update the validation in the
ChannelEngine (the require that now reads using candidate.homeLedger.chainId and
the version check guarded by ctx.prevState.version) to also require
candidate.version > 0 and enforce strict monotonicity (candidate.version >
ctx.prevState.version) unconditionally; specifically, replace the permissive OR
clause with a pair of checks that call IncorrectStateVersion() if
candidate.version == 0 or candidate.version <= ctx.prevState.version so behavior
matches EscrowDepositEngine/EscrowWithdrawalEngine.

In `@contracts/src/ChannelHub.sol`:
- Around line 1218-1226: The ETH transfer using payable(to).transfer(amount) in
_pushFunds can fail for contract recipients due to the 2300 gas stipend; replace
it with a low-level call: perform (bool success, ) = payable(to).call{value:
amount}("") and require success (e.g., require(success, "ETH transfer failed"))
so failures aren't silently ignored; keep the nonReentrant guard on _pushFunds
and preserve the existing ERC20 branch using IERC20(token).safeTransfer.
- Around line 1218-1226: The ETH send in _pushFunds uses
payable(to).transfer(amount) which can fail for contract recipients due to the
2300 gas stipend; replace that branch with a low-level call using (bool success,
) = to.call{value: amount}("") and require success (or revert with a clear
error) so the send works for contracts with non-trivial receive/fallback
functions; keep the nonReentrant guard and leave the ERC20 branch using
IERC20(...).safeTransfer as-is.
- Around line 641-644: The code recomputes _isHomeChain(meta.channelId) instead
of reusing the already-computed isHomeChain local, causing unnecessary SLOADs;
update both finalizeEscrowDeposit (replace the second
_isHomeChain(meta.channelId) call with the existing isHomeChain variable before
calling _processHomeChainEscrowFinalize) and finalizeEscrowWithdrawal (similarly
reuse isHomeChain instead of calling _isHomeChain again) so the cached boolean
is reused for branching.
- Around line 641-644: The code redundantly calls _isHomeChain(meta.channelId)
again instead of reusing the cached boolean isHomeChain; update
finalizeEscrowDeposit (around where require(candidate.intent ==
StateIntent.FINALIZE_ESCROW_DEPOSIT) and _processHomeChainEscrowFinalize is
invoked) to use the existing isHomeChain variable rather than calling
_isHomeChain again, and apply the same change in finalizeEscrowWithdrawal
(replace the second _isHomeChain(meta.channelId) call with the cached
isHomeChain) so you avoid extra SLOADs and duplicate calls.
- Around line 1204-1226: The _pullFunds and _pushFunds helpers are currently
marked nonReentrant which only protects each helper call individually; remove
the nonReentrant modifier from _pullFunds and _pushFunds and instead apply
nonReentrant to the external entry points that perform full operations (e.g.,
depositToChannel, withdrawFromChannel, closeChannel and any other
public/external APIs that call _applyTransitionEffects), so the reentrancy lock
spans the entire high-level operation; verify all external entry points that
perform token/ETH transfers are now decorated with nonReentrant and that
internal helpers (_pullFunds, _pushFunds, _applyTransitionEffects) remain plain
internal functions.
- Around line 383-402: The signed registration message in registerNodeValidator
currently omits replay protection; update the signature payload and validation
to include either a per-node nonce or an expiration timestamp (deadline) so old
signatures cannot be replayed. Modify registerNodeValidator to accept an
additional uint256 nonce or uint256 deadline parameter, include that value when
building the message (alongside validatorId, validator, block.chainid), and
verify the nonce/deadline before calling EcdsaSignatureUtils.validateEcdsaSigner
(e.g., check deadline >= block.timestamp or check nonce matches a stored mapping
and then increment it). Add the corresponding storage (mapping(address =>
uint256) nodeNonces or mapping for deadlines) and update state (increment nonce)
only after successful signature verification and registration, leaving
_nodeValidatorRegistry and the emit ValidatorRegistered unchanged.
- Around line 1204-1216: The nonReentrant modifier should be removed from
internal helpers like _pullFunds and _pushFunds and instead applied to the
external entry points (e.g., depositToChannel, withdrawFromChannel, closeChannel
and any other public/external functions that orchestrate multi-step operations)
so the reentrancy lock spans the entire high-level operation; update _pullFunds
and _pushFunds to be plain internal functions (no modifier), add nonReentrant to
each external API method that calls them, and run/adjust tests to ensure
msg.value checks and ERC20 safeTransferFrom calls still behave correctly under
the new locking placement.
- Around line 383-402: The registerNodeValidator signed payload currently uses
abi.encode(validatorId, validator, block.chainid) and lacks replay protection;
update registerNodeValidator to accept and validate either a nonce or deadline
(e.g., add uint256 nonce or uint256 deadline parameter) and include it in the
signed message passed to EcdsaSignatureUtils.validateEcdsaSigner so the
signature covers the timestamp/nonce as well; add storage to track nonces (e.g.,
mapping(address => uint256) _registrationNonces) or check deadline with
require(block.timestamp <= deadline) and ensure you increment the nonce or mark
it used before setting _nodeValidatorRegistry[node][validatorId] and emitting
ValidatorRegistered to prevent replay.

In `@contracts/src/EscrowWithdrawalEngine.sol`:
- Around line 155-159: Replace the incorrect revert error in the user net-flow
check: in EscrowWithdrawalEngine.sol locate the require that validates
candidate.nonHomeLedger.userNetFlow == 0 (the validation inside the withdrawal
logic) and change the thrown error from IncorrectNodeNetFlow() to
IncorrectUserNetFlow() so the revert matches the checked condition and existing
error types.
🧹 Nitpick comments (7)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@contracts/foundry.toml`:
- Around line 12-19: The comment claiming "prevent code size overflow" is now
stale given optimizer_runs = 20_000; update the comments around
additional_compiler_profiles and compilation_restrictions (the ChannelHub
profile and the ChannelHub.sol entry) to explain that ChannelHub is
intentionally compiled with high optimizer_runs to favor runtime gas efficiency
because the contract size was refactored to fit deployment limits, rather than
saying it prevents code size overflow; keep the references to name =
"channelhub" and optimizer_runs = 20_000 and adjust both comment lines to
reflect runtime optimization intent.

In `@contracts/src/ChannelHub.sol`:
- Around line 641-644: The code recomputes _isHomeChain(meta.channelId) instead
of reusing the already-computed isHomeChain local, causing unnecessary SLOADs;
update both finalizeEscrowDeposit (replace the second
_isHomeChain(meta.channelId) call with the existing isHomeChain variable before
calling _processHomeChainEscrowFinalize) and finalizeEscrowWithdrawal (similarly
reuse isHomeChain instead of calling _isHomeChain again) so the cached boolean
is reused for branching.
- Around line 641-644: The code redundantly calls _isHomeChain(meta.channelId)
again instead of reusing the cached boolean isHomeChain; update
finalizeEscrowDeposit (around where require(candidate.intent ==
StateIntent.FINALIZE_ESCROW_DEPOSIT) and _processHomeChainEscrowFinalize is
invoked) to use the existing isHomeChain variable rather than calling
_isHomeChain again, and apply the same change in finalizeEscrowWithdrawal
(replace the second _isHomeChain(meta.channelId) call with the cached
isHomeChain) so you avoid extra SLOADs and duplicate calls.
- Around line 1204-1226: The _pullFunds and _pushFunds helpers are currently
marked nonReentrant which only protects each helper call individually; remove
the nonReentrant modifier from _pullFunds and _pushFunds and instead apply
nonReentrant to the external entry points that perform full operations (e.g.,
depositToChannel, withdrawFromChannel, closeChannel and any other
public/external APIs that call _applyTransitionEffects), so the reentrancy lock
spans the entire high-level operation; verify all external entry points that
perform token/ETH transfers are now decorated with nonReentrant and that
internal helpers (_pullFunds, _pushFunds, _applyTransitionEffects) remain plain
internal functions.
- Around line 383-402: The signed registration message in registerNodeValidator
currently omits replay protection; update the signature payload and validation
to include either a per-node nonce or an expiration timestamp (deadline) so old
signatures cannot be replayed. Modify registerNodeValidator to accept an
additional uint256 nonce or uint256 deadline parameter, include that value when
building the message (alongside validatorId, validator, block.chainid), and
verify the nonce/deadline before calling EcdsaSignatureUtils.validateEcdsaSigner
(e.g., check deadline >= block.timestamp or check nonce matches a stored mapping
and then increment it). Add the corresponding storage (mapping(address =>
uint256) nodeNonces or mapping for deadlines) and update state (increment nonce)
only after successful signature verification and registration, leaving
_nodeValidatorRegistry and the emit ValidatorRegistered unchanged.
- Around line 1204-1216: The nonReentrant modifier should be removed from
internal helpers like _pullFunds and _pushFunds and instead applied to the
external entry points (e.g., depositToChannel, withdrawFromChannel, closeChannel
and any other public/external functions that orchestrate multi-step operations)
so the reentrancy lock spans the entire high-level operation; update _pullFunds
and _pushFunds to be plain internal functions (no modifier), add nonReentrant to
each external API method that calls them, and run/adjust tests to ensure
msg.value checks and ERC20 safeTransferFrom calls still behave correctly under
the new locking placement.
- Around line 383-402: The registerNodeValidator signed payload currently uses
abi.encode(validatorId, validator, block.chainid) and lacks replay protection;
update registerNodeValidator to accept and validate either a nonce or deadline
(e.g., add uint256 nonce or uint256 deadline parameter) and include it in the
signed message passed to EcdsaSignatureUtils.validateEcdsaSigner so the
signature covers the timestamp/nonce as well; add storage to track nonces (e.g.,
mapping(address => uint256) _registrationNonces) or check deadline with
require(block.timestamp <= deadline) and ensure you increment the nonce or mark
it used before setting _nodeValidatorRegistry[node][validatorId] and emitting
ValidatorRegistered to prevent replay.
contracts/foundry.toml (1)

12-19: Stale comment: "prevent code size overflow" no longer matches the 20,000 optimizer_runs intent.

With 20,000 optimizer_runs, the compiler optimizes heavily for runtime gas at the cost of larger deployment bytecode. The comments on lines 12 and 17 still reference preventing "code size overflow," which was relevant at 50 runs but is misleading now. Consider updating the comments to reflect that the refactored ChannelHub is small enough to afford higher optimizer_runs for better runtime efficiency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/foundry.toml` around lines 12 - 19, The comment claiming "prevent
code size overflow" is now stale given optimizer_runs = 20_000; update the
comments around additional_compiler_profiles and compilation_restrictions (the
ChannelHub profile and the ChannelHub.sol entry) to explain that ChannelHub is
intentionally compiled with high optimizer_runs to favor runtime gas efficiency
because the contract size was refactored to fit deployment limits, rather than
saying it prevents code size overflow; keep the references to name =
"channelhub" and optimizer_runs = 20_000 and adjust both comment lines to
reflect runtime optimization intent.
contracts/src/ChannelHub.sol (6)

641-644: Redundant _isHomeChain call — reuse the cached result.

_isHomeChain(meta.channelId) is already computed at line 625 and stored in isHomeChain, but line 643 re-invokes it, paying for duplicate SLOADs. The same pattern recurs in finalizeEscrowWithdrawal (line 719 vs 737).

Proposed fix (finalizeEscrowDeposit)
-        if (_isHomeChain(meta.channelId)) {
+        if (isHomeChain) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 641 - 644, The code recomputes
_isHomeChain(meta.channelId) instead of reusing the already-computed isHomeChain
local, causing unnecessary SLOADs; update both finalizeEscrowDeposit (replace
the second _isHomeChain(meta.channelId) call with the existing isHomeChain
variable before calling _processHomeChainEscrowFinalize) and
finalizeEscrowWithdrawal (similarly reuse isHomeChain instead of calling
_isHomeChain again) so the cached boolean is reused for branching.

641-644: Redundant _isHomeChain call — reuse the cached result.

_isHomeChain(meta.channelId) is already computed at line 625 and stored in isHomeChain, but line 643 re-invokes it, incurring duplicate SLOADs. The same pattern repeats in finalizeEscrowWithdrawal (lines 719 vs 737).

Proposed fix (finalizeEscrowDeposit)
-        require(candidate.intent == StateIntent.FINALIZE_ESCROW_DEPOSIT, IncorrectStateIntent());
+        require(candidate.intent == StateIntent.FINALIZE_ESCROW_DEPOSIT, IncorrectStateIntent());

-        if (_isHomeChain(meta.channelId)) {
-            _processHomeChainEscrowFinalize(meta.channelId, candidate, user, node);
+        if (isHomeChain) {
+            _processHomeChainEscrowFinalize(meta.channelId, candidate, user, node);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 641 - 644, The code redundantly
calls _isHomeChain(meta.channelId) again instead of reusing the cached boolean
isHomeChain; update finalizeEscrowDeposit (around where require(candidate.intent
== StateIntent.FINALIZE_ESCROW_DEPOSIT) and _processHomeChainEscrowFinalize is
invoked) to use the existing isHomeChain variable rather than calling
_isHomeChain again, and apply the same change in finalizeEscrowWithdrawal
(replace the second _isHomeChain(meta.channelId) call with the cached
isHomeChain) so you avoid extra SLOADs and duplicate calls.

1204-1226: Consider placing nonReentrant on external entry points instead of internal helpers.

Applying nonReentrant to _pullFunds/_pushFunds protects each individual external call, but releases the lock between sequential pulls and pushes within the same top-level transaction. This works today because there are no unguarded external calls in between, but the pattern is fragile — future modifications to _applyTransitionEffects or similar flows could silently introduce a reentrancy window. The conventional pattern is to guard the external entry points (depositToChannel, withdrawFromChannel, closeChannel, etc.) so the lock is held for the entire operation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 1204 - 1226, The _pullFunds and
_pushFunds helpers are currently marked nonReentrant which only protects each
helper call individually; remove the nonReentrant modifier from _pullFunds and
_pushFunds and instead apply nonReentrant to the external entry points that
perform full operations (e.g., depositToChannel, withdrawFromChannel,
closeChannel and any other public/external APIs that call
_applyTransitionEffects), so the reentrancy lock spans the entire high-level
operation; verify all external entry points that perform token/ETH transfers are
now decorated with nonReentrant and that internal helpers (_pullFunds,
_pushFunds, _applyTransitionEffects) remain plain internal functions.

383-402: No replay protection (nonce/deadline) in the signed registration message.

The signed message abi.encode(validatorId, validator, block.chainid) lacks a nonce or deadline. Currently safe because the ValidatorAlreadyRegistered check prevents re-registration, but if an unregisterNodeValidator function is ever added, old signatures could be replayed to re-register previously removed validators. Consider including a nonce or a deadline to future-proof the signature scheme.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 383 - 402, The signed registration
message in registerNodeValidator currently omits replay protection; update the
signature payload and validation to include either a per-node nonce or an
expiration timestamp (deadline) so old signatures cannot be replayed. Modify
registerNodeValidator to accept an additional uint256 nonce or uint256 deadline
parameter, include that value when building the message (alongside validatorId,
validator, block.chainid), and verify the nonce/deadline before calling
EcdsaSignatureUtils.validateEcdsaSigner (e.g., check deadline >= block.timestamp
or check nonce matches a stored mapping and then increment it). Add the
corresponding storage (mapping(address => uint256) nodeNonces or mapping for
deadlines) and update state (increment nonce) only after successful signature
verification and registration, leaving _nodeValidatorRegistry and the emit
ValidatorRegistered unchanged.

1204-1216: nonReentrant on internal helpers rather than external entry points is fragile.

The guard on _pullFunds/_pushFunds protects each individual external call, but the lock is released between sequential pull-then-push within the same top-level transaction. Today this is safe because no unguarded external calls exist in between, but a future change to _applyTransitionEffects (or similar) could silently introduce a reentrancy window. The conventional and more robust pattern is to guard the external entry points (depositToChannel, withdrawFromChannel, closeChannel, etc.) so the lock spans the entire operation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 1204 - 1216, The nonReentrant
modifier should be removed from internal helpers like _pullFunds and _pushFunds
and instead applied to the external entry points (e.g., depositToChannel,
withdrawFromChannel, closeChannel and any other public/external functions that
orchestrate multi-step operations) so the reentrancy lock spans the entire
high-level operation; update _pullFunds and _pushFunds to be plain internal
functions (no modifier), add nonReentrant to each external API method that calls
them, and run/adjust tests to ensure msg.value checks and ERC20 safeTransferFrom
calls still behave correctly under the new locking placement.

383-402: No replay protection (nonce/deadline) in the signed registration message.

The signed payload abi.encode(validatorId, validator, block.chainid) lacks a nonce or deadline. Currently safe because ValidatorAlreadyRegistered blocks re-registration, but if an unregisterNodeValidator function is added later, stale signatures could be replayed to re-register removed validators. Consider including a nonce or deadline to future-proof.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 383 - 402, The
registerNodeValidator signed payload currently uses abi.encode(validatorId,
validator, block.chainid) and lacks replay protection; update
registerNodeValidator to accept and validate either a nonce or deadline (e.g.,
add uint256 nonce or uint256 deadline parameter) and include it in the signed
message passed to EcdsaSignatureUtils.validateEcdsaSigner so the signature
covers the timestamp/nonce as well; add storage to track nonces (e.g.,
mapping(address => uint256) _registrationNonces) or check deadline with
require(block.timestamp <= deadline) and ensure you increment the nonce or mark
it used before setting _nodeValidatorRegistry[node][validatorId] and emitting
ValidatorRegistered to prevent replay.

Comment thread contracts/src/ChannelEngine.sol Outdated
Comment thread contracts/src/ChannelEngine.sol Outdated
Comment thread contracts/src/ChannelHub.sol
Comment thread contracts/src/EscrowWithdrawalEngine.sol
@nksazonov nksazonov force-pushed the refactor/channel-hub branch from 31e66fe to 9c6210b Compare February 17, 2026 10:02
@philanton philanton merged commit d0fad98 into main Feb 17, 2026
10 checks passed
@philanton philanton deleted the refactor/channel-hub branch February 17, 2026 10:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
contracts/src/ChannelHub.sol (3)

1204-1216: Redundant branching in _pullFunds — minor simplification possible.

The two separate if blocks on token == address(0) / token != address(0) could be collapsed into a single if/else.

Proposed fix
 function _pullFunds(address from, address token, uint256 amount) internal nonReentrant {
     if (amount == 0) return;

     if (token == address(0)) {
         require(msg.value == amount, IncorrectValue());
     } else {
         require(msg.value == 0, IncorrectValue());
-    }
-
-    if (token != address(0)) {
         IERC20(token).safeTransferFrom(from, address(this), amount);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 1204 - 1216, The _pullFunds
function has redundant branching for token == address(0) and token !=
address(0); simplify by converting the two separate if blocks into a single
if/else: check token == address(0) then require(msg.value == amount,
IncorrectValue()) in the if branch, else require(msg.value == 0,
IncorrectValue()) and call IERC20(token).safeTransferFrom(from, address(this),
amount) in the else branch, preserving the early return for amount == 0 and the
nonReentrant modifier and using the same IncorrectValue() error symbol.

383-402: Validator registration is one-way — no mechanism to unregister or rotate.

Once _nodeValidatorRegistry[node][validatorId] is set, there's no function to update or remove it. If a validator contract is compromised or deprecated, the node has no recourse except to stop including that validatorId in approvedSignatureValidators. Consider whether a deregisterNodeValidator (or update) function is needed.

The relayer-friendly signature-based pattern and cross-chain replay protection via block.chainid look solid otherwise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 383 - 402, registerNodeValidator
writes into _nodeValidatorRegistry with no way to change or remove entries; add
a complementary function (e.g., deregisterNodeValidator and/or
updateNodeValidator) that allows the node to remove or replace a validator entry
and emits a new event (e.g., ValidatorDeregistered or ValidatorUpdated).
Implement the same auth/signature pattern used in registerNodeValidator: accept
(address node, uint8 validatorId, ISignatureValidator validator, bytes calldata
signature) or for deregistration omit validator, validate via
EcdsaSignatureUtils.validateEcdsaSigner with abi.encode(validatorId, validator?,
block.chainid) and the node signer, enforce the same preconditions (reject
DEFAULT_SIG_VALIDATOR_ID, non-zero address rules as appropriate), update
_nodeValidatorRegistry[node][validatorId] accordingly, and emit the matching
event so rotations/removals are auditable.

1195-1202: _isHomeChain performs two storage reads for the same channel.

Line 1196 reads _channels[channelId].status and line 1201 reads _channels[channelId].lastState.homeLedger.chainId. Since _channels[channelId] is a storage mapping lookup, this involves separate SLOAD operations. The compiler may or may not optimize this. A minor gas improvement would be to use a single storage pointer.

Proposed fix
 function _isHomeChain(bytes32 channelId) internal view returns (bool) {
-    ChannelStatus status = _channels[channelId].status;
+    ChannelMeta storage meta = _channels[channelId];
+    ChannelStatus status = meta.status;
     if (status == ChannelStatus.VOID || status == ChannelStatus.MIGRATED_OUT) {
         return false;
     }
-    return _channels[channelId].lastState.homeLedger.chainId == block.chainid;
+    return meta.lastState.homeLedger.chainId == block.chainid;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 1195 - 1202, The function
_isHomeChain does two separate storage reads on _channels[channelId]; to reduce
SLOADs, load the struct once into a storage pointer (e.g., Channel storage ch =
_channels[channelId]) and then use ch.status and ch.lastState.homeLedger.chainId
for the checks; update references in _isHomeChain accordingly so status and
lastState are read from that single storage pointer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/src/ChannelHub.sol`:
- Around line 290-306: You removed the public getters
getUnlockableEscrowDepositAmount and getUnlockableEscrowDepositCount and
replaced them with an internal helper getUnlockableEscrowDepositStats, breaking
the external ABI; restore backward compatibility by adding external/public
wrapper functions named getUnlockableEscrowDepositAmount() and
getUnlockableEscrowDepositCount() that call getUnlockableEscrowDepositStats()
and return the appropriate single value (totalAmount or count), or if you
intentionally want to remove them, regenerate and commit the updated ABI
(sdk/ts/src/blockchain/evm/channel_hub_abi.ts) so off-chain consumers match the
contract change.

---

Duplicate comments:
In `@contracts/src/ChannelEngine.sol`:
- Around line 99-102: The version check in _validateUniversal incorrectly allows
a candidate.version == 0 when ctx.prevState is empty due to Utils.isEmpty
short-circuiting; update _validateUniversal (TransitionContext, State) to
explicitly require candidate.version > 0 for fresh channels — e.g. branch on
Utils.isEmpty(ctx.prevState) and enforce candidate.version > 0 for empty
prevState, otherwise require candidate.version > ctx.prevState.version — so that
candidate.version 0 is rejected on new channels and behavior matches the escrow
engines.
- Around line 480-533: The review notes an inconsistency in error types for
net-flow delta checks inside _calculateFinalizeMigrationEffects: replace the
incorrect error calls with the standardized ones; specifically ensure the checks
that validate userNfDelta and nodeNfDelta use IncorrectUserNetFlowDelta() and
IncorrectNodeNetFlowDelta() respectively (these checks are the require(...)
statements immediately after the comment about special delta calculation), so
the function consistently throws the same error types used elsewhere in the
contract.

In `@contracts/src/ChannelHub.sol`:
- Around line 1218-1227: The push-funds logic in _pushFunds correctly replaced
.transfer with a low-level payable(to).call{value: amount}("") and checks
success, and nonReentrant is present; no changes required—keep function
_pushFunds as implemented (references: _pushFunds, nonReentrant,
IERC20.safeTransfer) and ensure the same success check and ERC20 fallback remain
in place.

In `@contracts/src/EscrowWithdrawalEngine.sol`:
- Around line 155-157: The require checks in EscrowWithdrawalEngine (checking
ctx.status == EscrowStatus.VOID, candidate.nonHomeLedger.userAllocation == 0,
and candidate.nonHomeLedger.userNetFlow == 0) now correctly use the matching
error types (e.g., IncorrectUserNetFlow() for userNetFlow) so no code change is
needed—leave the require on candidate.nonHomeLedger.userNetFlow as
IncorrectUserNetFlow() and keep the other checks as-is.

---

Nitpick comments:
In `@contracts/src/ChannelHub.sol`:
- Around line 1204-1216: The _pullFunds function has redundant branching for
token == address(0) and token != address(0); simplify by converting the two
separate if blocks into a single if/else: check token == address(0) then
require(msg.value == amount, IncorrectValue()) in the if branch, else
require(msg.value == 0, IncorrectValue()) and call
IERC20(token).safeTransferFrom(from, address(this), amount) in the else branch,
preserving the early return for amount == 0 and the nonReentrant modifier and
using the same IncorrectValue() error symbol.
- Around line 383-402: registerNodeValidator writes into _nodeValidatorRegistry
with no way to change or remove entries; add a complementary function (e.g.,
deregisterNodeValidator and/or updateNodeValidator) that allows the node to
remove or replace a validator entry and emits a new event (e.g.,
ValidatorDeregistered or ValidatorUpdated). Implement the same auth/signature
pattern used in registerNodeValidator: accept (address node, uint8 validatorId,
ISignatureValidator validator, bytes calldata signature) or for deregistration
omit validator, validate via EcdsaSignatureUtils.validateEcdsaSigner with
abi.encode(validatorId, validator?, block.chainid) and the node signer, enforce
the same preconditions (reject DEFAULT_SIG_VALIDATOR_ID, non-zero address rules
as appropriate), update _nodeValidatorRegistry[node][validatorId] accordingly,
and emit the matching event so rotations/removals are auditable.
- Around line 1195-1202: The function _isHomeChain does two separate storage
reads on _channels[channelId]; to reduce SLOADs, load the struct once into a
storage pointer (e.g., Channel storage ch = _channels[channelId]) and then use
ch.status and ch.lastState.homeLedger.chainId for the checks; update references
in _isHomeChain accordingly so status and lastState are read from that single
storage pointer.

Comment on lines +290 to 306
function getUnlockableEscrowDepositStats() internal view returns (uint256 count, uint256 totalAmount) {
uint256 totalDeposits = _escrowDepositIds.length;
uint256 escrowHeadTemp = escrowHead;

while (escrowHeadTemp < totalDeposits) {
bytes32 escrowId = _escrowDepositIds[escrowHeadTemp];
EscrowDepositMeta storage meta = _escrowDeposits[escrowId];

if (meta.unlockAt <= block.timestamp && meta.status == EscrowStatus.INITIALIZED) {
if (_isEscrowDepositUnlockable(meta)) {
count++;
totalAmount += meta.lockedAmount;
escrowHeadTemp++;
} else {
break;
}

escrowHeadTemp++;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all references to getUnlockableEscrowDeposit functions
rg 'getUnlockableEscrowDeposit' -g '*.sol' -g '*.ts' -g '*.js' -A 2 -B 2

Repository: erc7824/nitrolite

Length of output: 1127


🏁 Script executed:

#!/bin/bash
# Check ChannelHub.sol for the full function history and any old function definitions
cat -n contracts/src/ChannelHub.sol | grep -A 10 -B 5 'getUnlockableEscrowDeposit'

Repository: erc7824/nitrolite

Length of output: 807


🏁 Script executed:

#!/bin/bash
# Look for any test files that might reference these functions
fd -e 'test.ts' -o -e 'test.js' -o -e 'spec.ts' -o -e 'spec.js' | xargs grep -l 'getUnlockableEscrowDeposit' 2>/dev/null || echo "No test files found with these references"

Repository: erc7824/nitrolite

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Search for complete function definitions of old functions
rg -n 'function getUnlockableEscrowDepositAmount|function getUnlockableEscrowDepositCount' contracts/src/ChannelHub.sol

Repository: erc7824/nitrolite

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Get more context around the escrow deposit functions in ChannelHub.sol
rg -n 'getUnlockableEscrow|function.*Escrow.*Deposit' contracts/src/ChannelHub.sol -B 2 -A 8

Repository: erc7824/nitrolite

Length of output: 5052


🏁 Script executed:

#!/bin/bash
# Check the ABI file to see what functions are still defined as public
cat sdk/ts/src/blockchain/evm/channel_hub_abi.ts | grep -A 5 'getUnlockableEscrowDeposit'

Repository: erc7824/nitrolite

Length of output: 338


This is a breaking change—old public functions getUnlockableEscrowDepositAmount and getUnlockableEscrowDepositCount have been removed and replaced with an internal function.

The old functions are completely absent from ChannelHub.sol but remain defined in the ABI at sdk/ts/src/blockchain/evm/channel_hub_abi.ts. Any off-chain consumers (TypeScript SDK, dApps) calling these functions will fail. The ABI must be regenerated or the contract must maintain backward-compatible wrappers (e.g., external functions delegating to getUnlockableEscrowDepositStats).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 290 - 306, You removed the public
getters getUnlockableEscrowDepositAmount and getUnlockableEscrowDepositCount and
replaced them with an internal helper getUnlockableEscrowDepositStats, breaking
the external ABI; restore backward compatibility by adding external/public
wrapper functions named getUnlockableEscrowDepositAmount() and
getUnlockableEscrowDepositCount() that call getUnlockableEscrowDepositStats()
and return the appropriate single value (totalAmount or count), or if you
intentionally want to remove them, regenerate and commit the updated ABI
(sdk/ts/src/blockchain/evm/channel_hub_abi.ts) so off-chain consumers match the
contract change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants