Skip to content

Comments

YNU-760: feat(ChannelHub): reclaim logic on failed transfers#586

Merged
nksazonov merged 11 commits intomainfrom
fix/erc20-transfer-fail
Feb 25, 2026
Merged

YNU-760: feat(ChannelHub): reclaim logic on failed transfers#586
nksazonov merged 11 commits intomainfrom
fix/erc20-transfer-fail

Conversation

@nksazonov
Copy link
Contributor

@nksazonov nksazonov commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • Outbound transfer failures no longer revert; failed amounts are tracked as reclaimable balances and can be claimed to alternative destinations.
    • Transfers execute with an explicit gas limit to reduce gas-depletion risks.
  • Documentation

    • Protocol and security docs expanded to describe transfer resilience, reclaim mechanics, and mitigations.
  • Tests

    • Extensive tests and mocks added to validate reclaim flows and diverse token/recipient failure scenarios.

@nksazonov nksazonov requested a review from a team as a code owner February 25, 2026 08:29
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

@nksazonov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 56 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b87af1e and 0553324.

📒 Files selected for processing (2)
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_claimFunds.t.sol
📝 Walkthrough

Walkthrough

Replaces error-based outbound-transfer failures with event-driven reclaim tracking, adds TRANSFER_GAS_LIMIT, _reclaims state, getReclaimBalance and claimFunds APIs, plus tests and mocks for varied ERC20/ETH failure modes. Also duplicates a documentation block in contracts/SECURITY.md and adjusts a build/import setting.

Changes

Cohort / File(s) Summary
Core Contract
contracts/src/ChannelHub.sol
Add TRANSFER_GAS_LIMIT, _reclaims mapping, TransferFailed & FundsClaimed events, getReclaimBalance and claimFunds. Outbound transfers use gas limit and accumulate failures into _reclaims and emit events instead of reverting; removed old TransferFailed error.
Tests — Push/Claim
contracts/test/ChannelHub_pushFunds.t.sol, contracts/test/ChannelHub_claimFunds.t.sol, contracts/test/TestChannelHub.sol
New test suites covering push/claim flows and failure scenarios. TestChannelHub exposes internal push/pull wrappers and a test-only setter to manipulate _reclaims.
Mocks — ERC20
contracts/test/mocks/NonReturningERC20.sol, contracts/test/mocks/RevertingERC20.sol, contracts/test/mocks/GasConsumingERC20.sol, contracts/test/mocks/MalformedReturningERC20.sol
Add mock tokens to simulate non-returning, reverting, gas-consuming, and malformed-return behaviors used to validate transfer resilience and reclaim accumulation.
Mocks — ETH Receivers
contracts/test/mocks/RevertingEthReceiver.sol, contracts/test/mocks/GasConsumingEthReceiver.sol
Add ETH receiver mocks that revert or consume all gas to test native ETH transfer failure handling and reclaim behavior.
Documentation & Protocol
contracts/SECURITY.md, protocol-description.md
Add transfer-failure resilience documentation; contracts/SECURITY.md contains a duplicated "ERC20 Transfer Failure Attack Vectors" section (identical block appears twice).
Build / Imports
contracts/foundry.toml, contracts/src/ChannelEngine.sol
Reduce ChannelHub optimizer runs (20,000→4,000) in foundry config; remove unused Ledger import from ChannelEngine.sol.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Hub as ChannelHub
    participant Token as Token/ETHReceiver
    participant Reclaim as ReclaimStore

    User->>Hub: pushFunds(recipient, token, amount)
    activate Hub
    Hub->>Token: transfer/call (gas: TRANSFER_GAS_LIMIT)
    alt transfer succeeds
        Token-->>Hub: success
        Hub->>Reclaim: apply payment / update balances
        Hub-->>User: operation completes
    else transfer fails
        Token-->>Hub: failure/revert
        Hub->>Reclaim: _reclaims[account][token] += amount
        Hub-->>User: emit TransferFailed event
    end
    deactivate Hub

    User->>Hub: claimFunds(token, destination)
    activate Hub
    Hub->>Reclaim: read reclaimAmount = _reclaims[user][token]
    Hub->>Token: transfer reclaimAmount => destination (gas-limited)
    Hub->>Reclaim: _reclaims[user][token] = 0
    Hub-->>User: emit FundsClaimed event
    deactivate Hub
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ready

Suggested reviewers

  • philanton
  • dimast-x
  • alessio

Poem

🐰 I hopped through code and chased each cent,
Gas trimmed to a limit, no transfer is bent,
Failures nest soft in reclaims for later,
Events hum the ledger — a rabbit's translator,
Claim, hop, repeat — resilient and greater.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: implementing reclaim logic in ChannelHub to handle failed transfers, which is supported by the extensive changes to ChannelHub.sol and comprehensive test coverage.
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 fix/erc20-transfer-fail

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
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 significantly enhances the robustness and security of the ChannelHub contract by addressing critical vulnerabilities related to outbound transfer failures. By implementing a reclaim mechanism and gas limiting for transfers, the protocol can now gracefully handle scenarios like token blacklists, malicious recipient hooks, and gas depletion attacks without reverting transactions or locking funds. This ensures continuous channel operation and protects the Node from potential financial losses, improving the overall stability and reliability of the system.

Highlights

  • Reclaim Logic for Failed Transfers: Implemented a 'reclaim pattern' where outbound transfers that fail (due to blacklists, hooks, or other issues) no longer revert the transaction. Instead, the funds are accumulated in a dedicated mapping for the recipient to claim later.
  • Gas Depletion Attack Prevention: Introduced a TRANSFER_GAS_LIMIT (100,000 gas) for all outbound native ETH and ERC20 transfers. This prevents malicious recipient contracts from consuming all available gas and causing transaction reverts, which could lead to funds being locked.
  • New User Claim Functionality: Added a claimFunds external function, allowing users to retrieve any tokens or native ETH that were accumulated in their reclaim balance due to previously failed transfers.
  • Enhanced Security Documentation: Updated the SECURITY.md and protocol-description.md files to thoroughly document the new ERC20 transfer failure attack vectors (Channel Lifecycle Stuck, Node Funds Lock Attack) and explain how the reclaim pattern and gas limiting mitigate these risks.
  • Comprehensive Testing: Added extensive unit tests for the _pushFunds internal function, covering various scenarios including successful transfers, zero-amount transfers, and failed transfers involving reverting, non-returning, and gas-consuming ERC20 tokens and ETH receivers.
Changelog
  • contracts/SECURITY.md
    • Added detailed sections on ERC20 Transfer Failure Attack Vectors, including 'Channel Lifecycle Stuck' and 'Node Funds Lock Attack'.
    • Documented the 'Solution: Reclaim Pattern' and 'Gas Depletion Attacks' with their respective mitigations.
  • contracts/src/ChannelHub.sol
    • Added TransferFailed and FundsClaimed events for transparency on transfer outcomes.
    • Removed the TransferFailed error, as transfers no longer revert on failure.
    • Introduced TRANSFER_GAS_LIMIT constant to cap gas usage for outbound transfers.
    • Added _reclaims mapping to store funds from failed transfers.
    • Implemented getReclaimBalance view function to query accumulated reclaimable funds.
    • Added claimFunds external function for users to retrieve their reclaimable funds.
    • Modified _pushFunds to use the TRANSFER_GAS_LIMIT and accumulate failed transfers in _reclaims instead of reverting.
  • contracts/test/ChannelHub_pushFunds.t.sol
    • Added a new test file to verify the new _pushFunds logic.
    • Included tests for successful ERC20 and native ETH transfers.
    • Verified correct accumulation of funds in _reclaims for various failure scenarios (reverting, non-returning, gas-consuming tokens/receivers).
    • Ensured zero-amount transfers are handled as no-ops.
  • contracts/test/TestChannelHub.sol
    • Added a new test harness contract to expose internal _pushFunds and _pullFunds functions for testing.
  • contracts/test/mocks/GasConsumingERC20.sol
    • Added a mock ERC20 token that consumes all available gas during transfer for testing gas depletion attacks.
  • contracts/test/mocks/GasConsumingEthReceiver.sol
    • Added a mock contract that consumes all available gas on ETH receive for testing native ETH gas depletion attacks.
  • contracts/test/mocks/NonReturningERC20.sol
    • Added a mock ERC20 token that does not return a boolean on transfer, simulating tokens like USDT.
  • contracts/test/mocks/RevertingERC20.sol
    • Added a mock ERC20 token that always reverts on transfer, simulating blacklisted tokens.
  • contracts/test/mocks/RevertingEthReceiver.sol
    • Added a mock contract that reverts on ETH receive for testing.
  • protocol-description.md
    • Updated the 'Resilience' section to include 'Transfer failure resilience', detailing the reclaim pattern and gas limiting as key protective measures.
Activity
  • The pull request introduces significant changes to the ChannelHub contract's transfer mechanisms to enhance security and resilience.
  • New events and storage mappings were added to support the reclaim logic.
  • A new external function claimFunds was implemented for users to interact with the new reclaim system.
  • The core _pushFunds internal function was refactored to incorporate gas limiting and the reclaim pattern.
  • Extensive new test files and mock contracts were added to validate the new transfer and reclaim logic under various conditions, including adversarial ones.
  • Documentation in SECURITY.md and protocol-description.md was updated to reflect the new security considerations and solutions.
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.

@codecov
Copy link

codecov bot commented Feb 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
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 introduces a crucial security enhancement by implementing a "reclaim" mechanism for failed outbound transfers in the ChannelHub contract. This change prevents transaction reverts on transfer failures, mitigating serious fund-locking and denial-of-service attack vectors. The solution involves limiting gas on outbound transfers and accumulating failed transfer amounts for users to claim later. The implementation is well-documented and accompanied by a comprehensive test suite. I've identified a couple of areas for improvement to enhance the robustness and correctness of the implementation.

Copy link
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: 3

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

290-302: withdrawFromVault now silently succeeds even when the actual transfer fails.

With the new _pushFunds behavior, if the to address can't receive funds (e.g., blacklisted ERC20 recipient or gas-consuming receiver), _nodeBalances is decremented and the Withdrawn event fires, but funds land in _reclaims[to] instead of arriving at to. The caller gets no on-chain signal of failure in the return value—only a TransferFailed event.

This is a behavior change worth documenting explicitly (e.g., in a NatDoc on withdrawFromVault), so integrators know to watch for TransferFailed alongside Withdrawn.

🤖 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 - 302, The withdrawFromVault
function currently decrements _nodeBalances and emits Withdrawn even if
_pushFunds fails and the funds are routed to _reclaims (signaled by
TransferFailed); update the contract docs (NatSpec) for withdrawFromVault to
explicitly document this behavior: describe that a successful call only means
internal balance was decreased and Withdrawn emitted, but transfers may still
fail and result in funds placed in _reclaims, and instruct integrators to
monitor TransferFailed events (and optionally reclaims handling) when calling
withdrawFromVault; reference withdrawFromVault, _pushFunds, _reclaims,
Withdrawn, and TransferFailed in the NatSpec.
🤖 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 310-328: The claimFunds function is marked payable but never reads
msg.value, which lets callers accidentally send ETH that becomes stuck; remove
the payable modifier from the claimFunds declaration so the function cannot
receive native ETH, leaving the rest of the logic (the ETH-transfer branch using
token == address(0) and IERC20 flow) unchanged and ensuring no code relies on
msg.value in claimFunds.
- Around line 1272-1284: The ERC20 gas-limited call in the transfer path can
misinterpret a call to an EOA (no code) as success because (true, "") passes the
current check; update the success logic in the block using TRANSFER_GAS_LIMIT so
that when returndata.length == 0 you also verify the target token address
contains code (i.e., extcodesize > 0) before treating it as a successful
transfer. Concretely, modify the transferSucceeded computation (the code using
address(token).call, returndata, success) to require either decoded true OR
(returndata empty AND token is a contract) — use Address.isContract(token) or an
inline extcodesize check — and leave the reclaim logic (_reclaims[to][token] +=
amount and emit TransferFailed) unchanged.

In `@contracts/test/ChannelHub_pushFunds.t.sol`:
- Around line 98-183: Add comprehensive tests for ChannelHub.claimFunds: create
tests that (1) fund a reclaim via cHub.exposed_pushFunds then call
cHub.claimFunds() as the recipient (destination == msg.sender) for both ETH
(address(0)) and an ERC20 (normalToken) and assert balances changed and
FundsClaimed event emitted; (2) call claimFunds specifying a different
destination (destination != msg.sender) to exercise the blacklisted/third-party
flow and assert funds delivered and event emitted; (3) assert claimFunds reverts
when destination == address(0); (4) assert claimFunds reverts when reclaim
balance is zero; and (5) verify reentrancy protection by funding a malicious
receiver contract that attempts to reenter claimFunds during transfer and assert
the reentrant call fails/does not drain funds. Reference functions and symbols:
claimFunds, FundsClaimed, ChannelHub, cHub, exposed_pushFunds, normalToken,
recipient, TRANSFER_AMOUNT, and any test helpers used for balance assertions.

---

Nitpick comments:
In `@contracts/src/ChannelHub.sol`:
- Around line 290-302: The withdrawFromVault function currently decrements
_nodeBalances and emits Withdrawn even if _pushFunds fails and the funds are
routed to _reclaims (signaled by TransferFailed); update the contract docs
(NatSpec) for withdrawFromVault to explicitly document this behavior: describe
that a successful call only means internal balance was decreased and Withdrawn
emitted, but transfers may still fail and result in funds placed in _reclaims,
and instruct integrators to monitor TransferFailed events (and optionally
reclaims handling) when calling withdrawFromVault; reference withdrawFromVault,
_pushFunds, _reclaims, Withdrawn, and TransferFailed in the NatSpec.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5c378 and 85e3ef7.

📒 Files selected for processing (10)
  • contracts/SECURITY.md
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_pushFunds.t.sol
  • contracts/test/TestChannelHub.sol
  • contracts/test/mocks/GasConsumingERC20.sol
  • contracts/test/mocks/GasConsumingEthReceiver.sol
  • contracts/test/mocks/NonReturningERC20.sol
  • contracts/test/mocks/RevertingERC20.sol
  • contracts/test/mocks/RevertingEthReceiver.sol
  • protocol-description.md

Copy link
Contributor

@philanton philanton left a comment

Choose a reason for hiding this comment

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

So, are we going to allow them withdraw to another address right now?)

@philanton
Copy link
Contributor

So, are we going to allow them withdraw to another address right now?)

Oh, or are we allowing withdrawal even if the sender is not the original owner of these funds? So, like, we could withdraw it ourselves?))

Copy link
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

🤖 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 1279-1281: The current transfer success check uses "balanceAfter
== balanceBefore - amount" which can underflow and revert if balanceBefore <
amount; change the logic that computes transferSucceeded (using variables
transferSucceeded, balanceBefore, balanceAfter, amount, success) to first ensure
balanceBefore >= amount (e.g., include balanceBefore >= amount in the boolean
expression) before performing the subtraction so the contract doesn't revert and
the subsequent reclaim logic can run.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85e3ef7 and 4ef2449.

📒 Files selected for processing (7)
  • contracts/foundry.toml
  • contracts/src/ChannelEngine.sol
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_claimFunds.t.sol
  • contracts/test/ChannelHub_pushFunds.t.sol
  • contracts/test/TestChannelHub.sol
  • contracts/test/mocks/MalformedReturningERC20.sol
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/test/TestChannelHub.sol

@dimast-x
Copy link
Contributor

Having the same question as Anton. But overall approved.

@nksazonov
Copy link
Contributor Author

@philanton , @dimast-x , the claimFunds logic is as follows:
Only an address owning the funds can claim them back, but they can supply a destination address for those funds to be transferred to.
The reasoning is in this article at the bottom.

Copy link
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.

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

321-321: Prefer custom error over string revert for ETH claim failure.

This keeps revert handling consistent with the rest of the contract and trims bytecode/gas.

Proposed diff
@@
     error IncorrectStateIntent();
     error IncorrectChannelStatus();
     error ChallengerVersionTooLow();
     error OnlyNonHomeEscrowsCanBeChallenged();
+    error EthTransferFailed();
@@
         if (token == address(0)) {
             (bool success,) = payable(destination).call{value: amount}("");
-            require(success, "ETH transfer failed");
+            if (!success) revert EthTransferFailed();
         } else {
             IERC20(token).safeTransfer(destination, amount);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` at line 321, Replace the string revert in the
ETH transfer require with a custom error: declare a new error (e.g., error
ETHTransferFailed();) near the top of ChannelHub contract and change the
require(success, "ETH transfer failed"); to revert ETHTransferFailed(); (or use
if (!success) revert ETHTransferFailed();). This keeps revert handling
consistent and reduces bytecode/gas.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@contracts/src/ChannelHub.sol`:
- Line 321: Replace the string revert in the ETH transfer require with a custom
error: declare a new error (e.g., error ETHTransferFailed();) near the top of
ChannelHub contract and change the require(success, "ETH transfer failed"); to
revert ETHTransferFailed(); (or use if (!success) revert ETHTransferFailed();).
This keeps revert handling consistent and reduces bytecode/gas.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef2449 and b87af1e.

📒 Files selected for processing (2)
  • contracts/src/ChannelHub.sol
  • contracts/test/TestChannelHub.sol

@nksazonov nksazonov merged commit 1c17cc7 into main Feb 25, 2026
11 checks passed
@nksazonov nksazonov deleted the fix/erc20-transfer-fail branch February 25, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants