Skip to content

H-L02: fix(contracts/ChannelHub): revert on withdrawFromVault failure#651

Merged
nksazonov merged 2 commits into
fix/audit-findingsfrom
fix/audit-h-l02
Apr 7, 2026
Merged

H-L02: fix(contracts/ChannelHub): revert on withdrawFromVault failure#651
nksazonov merged 2 commits into
fix/audit-findingsfrom
fix/audit-h-l02

Conversation

@nksazonov
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov commented Apr 6, 2026

Description

A node can permanently lock their own vault funds by calling withdrawFromVault(..) with a contract address that:

  1. causes the transfer to fail
  2. cannot call claimFunds(..) to recover the funds from reclaims

At L304 in withdrawFromVault(..), the node's vault balance is reduced regardless of transfer success. When the transfer fails in _pushFunds(..) (L1322-1324 for ETH, L1342-1344 for ERC20), the funds are moved to _reclaims[contractAddress][token]. However, if contractAddress is a contract without the capability to call claimFunds(..) (no function to make external calls, old contract, simple escrow, etc.), the funds become permanently inaccessible. The core issue is architectural: withdrawFromVault(..) uses the reclaim mechanism designed for adversarial channel operations in a non-adversarial context where it creates unexpected partial-success behavior. The node's vault balance is reduced even when the transfer fails, violating standard atomicity expectations where operations should either succeed completely or fail completely.

Impact

A node operator can permanently lose access to their entire vault balance through withdrawFromVault(..) when the destination address cannot receive the transfer or claim from reclaims. The operation exhibits partial-success behavior: the transaction succeeds (doesn't revert), the vault balance is reduced, but the funds end up in reclaims at an address that may not be able to recover them. This is non-standard behavior that violates atomicity principles - users cannot distinguish between 'transfer succeeded' and 'transfer failed but balance still reduced' without explicitly checking reclaim balances. The maximum impact is the node's entire vault balance across all tokens. While this requires user error (choosing an invalid destination), the protocol's design amplifies this error by using partial-success behavior instead of reverting.

Summary by CodeRabbit

  • Refactor

    • Enhanced fund transfer resilience by introducing a non-reverting transfer mechanism; failed transfers are now tracked and recoverable instead of reverting transactions.
  • Tests

    • Added comprehensive test coverage for vault deposit and withdrawal operations with ERC20 and native ETH asset support.
    • Added tests for transfer failure scenarios and recovery mechanisms to ensure atomicity and proper error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19ee8efd-1537-401c-9071-eea01e48790c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces a dual-function approach to fund transfers in ChannelHub: a reverting _pushFunds function for critical paths and a non-reverting _nonRevertingPushFunds function with reclaim-on-failure handling for settlement, closure, and escrow paths. Comprehensive withdrawal tests are added alongside test infrastructure updates.

Changes

Cohort / File(s) Summary
Core Transfer Logic
contracts/src/ChannelHub.sol
Introduced _pushFunds (reverts on failure) and _nonRevertingPushFunds (stores failed transfers in _reclaims mapping) for both native ETH and ERC20 tokens. Replaced multiple _pushFunds calls with _nonRevertingPushFunds in channel settlement/closure and escrow finalization paths where user funds are released.
Vault Withdrawal Tests
contracts/test/ChannelHub_IVault.t.sol
New comprehensive test suite covering input validation (zero address, zero amount, insufficient balance), successful withdrawals for both ERC20 and native ETH, event emission verification, full-balance withdrawals, and failure atomicity for both token types with reclaim balance verification.
Test Infrastructure
contracts/test/ChannelHub_Base.t.sol, contracts/test/TestChannelHub.sol, contracts/test/ChannelHub_nonRevertingPushFunds.t.sol
Updated test setup to include ETH vault deposit, replaced exposed testing wrapper exposed_pushFunds with exposed_nonRevertingPushFunds, added workaround_setNodeBalance helper, and renamed/updated test contract to target non-reverting variant.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • dimast-x
  • philanton
  • ihsraham

Poem

🐰 Hop, hop, transfers now take two paths clear—
One reverts when funds disappear,
The other reclaims with graceful care,
No loss of coin in the rabbit's lair!

🚥 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 title clearly describes the main change: fixing withdrawFromVault to revert on transfer failure, which directly addresses the architectural issue documented in the PR objectives of preventing permanent fund loss due to partial-success behavior.
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
  • Commit unit tests in branch fix/audit-h-l02

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.

@nksazonov
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

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

⚠️ Outside diff range comments (1)
contracts/src/ChannelHub.sol (1)

1388-1392: ⚠️ Potential issue | 🔴 Critical

_nonRevertingPushFunds has a revert path via _trySafeTransfer() that violates its "never reverts" guarantee.

The function's documentation states it is never reverts and is used in adversarial settlement contexts. However, _trySafeTransfer() decodes the low-level call's return data as bool using abi.decode(returnData, (bool)). This operation reverts if the bytes do not represent a canonical boolean value (0 or 1). A non-compliant ERC20 token returning a non-canonical value (e.g., 0xFF) would trigger this revert, violating the critical guarantee on settlement/close paths.

Fix: Decode as uint256 and check equality
-        if (returnData.length >= 32) return abi.decode(returnData, (bool));
+        if (returnData.length >= 32) {
+            return abi.decode(returnData, (uint256)) == 1;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/ChannelHub.sol` around lines 1388 - 1392,
_nonRevertingPushFunds relies on _trySafeTransfer to never revert but
_trySafeTransfer currently abi.decode(returnData, (bool)) which can revert on
non-canonical ERC20 returns; update _trySafeTransfer to decode returnData as
uint256 (e.g., abi.decode(returnData, (uint256))) and treat a value of 1 (or any
non-zero convention you choose) as success, also handle empty returnData as
success for tokens that return nothing; keep the same failure path that
increments _reclaims and emits TransferFailed when transfer is considered
unsuccessful so _nonRevertingPushFunds retains its "never reverts" guarantee.
🤖 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 1142-1145: The cooperative/signed payout branches currently call
_nonRevertingPushFunds which converts failures into _reclaims and can
permanently strand funds for users who can't call claimFunds(); instead, for
voluntary releases (e.g., paths reached from withdrawFromChannel, closeChannel,
signed escrow finalization) replace calls to _nonRevertingPushFunds with
_pushFunds so a failing push reverts the transaction and does not move balances
into _reclaims; keep _nonRevertingPushFunds only on explicit
adversarial/timeout/challenge settlement branches where reclaim semantics are
intended, and ensure you still adjust meta.lockedFunds and do not change how
_reclaims is written in those adversarial branches.

---

Outside diff comments:
In `@contracts/src/ChannelHub.sol`:
- Around line 1388-1392: _nonRevertingPushFunds relies on _trySafeTransfer to
never revert but _trySafeTransfer currently abi.decode(returnData, (bool)) which
can revert on non-canonical ERC20 returns; update _trySafeTransfer to decode
returnData as uint256 (e.g., abi.decode(returnData, (uint256))) and treat a
value of 1 (or any non-zero convention you choose) as success, also handle empty
returnData as success for tokens that return nothing; keep the same failure path
that increments _reclaims and emits TransferFailed when transfer is considered
unsuccessful so _nonRevertingPushFunds retains its "never reverts" guarantee.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 477d09f5-ad79-4c45-b1e0-ff845a86626b

📥 Commits

Reviewing files that changed from the base of the PR and between e39b229 and cd0a732.

📒 Files selected for processing (5)
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_Base.t.sol
  • contracts/test/ChannelHub_IVault.t.sol
  • contracts/test/ChannelHub_nonRevertingPushFunds.t.sol
  • contracts/test/TestChannelHub.sol

Comment thread contracts/src/ChannelHub.sol
@nksazonov nksazonov merged commit b5a2d9d into fix/audit-findings Apr 7, 2026
3 checks passed
@nksazonov nksazonov deleted the fix/audit-h-l02 branch April 7, 2026 13:49
nksazonov added a commit that referenced this pull request Apr 16, 2026
H-H01: fix(contracts): disallow challenge with CLOSE or FIN_MIG intents (#631)

H-M01: fix(contracts/ChannelHub): remove transferred amount check (#636)

fix(contracts/ChannelHub): allow unblocking escrow ops after migration (#637)

M-C01: feat(contracts): add token check between states (#639)

H-L03: fix(contracts/ChannelHub): skip disputed escrow during purge (#640)

M-H04: feat(contracts/ChannelHub): purge during escrow challenge finalization, count both skip and purge (#641)

M-M02: fix(contracts): require zero allocs on close (#643)

M-C02: fix(rpc/core): apply finalize escrow deposit correctly (#644)

H-L02: fix(contracts/ChannelHub): revert on withdrawFromVault failure (#651)

M-H03: fix(clearnode): log correct fields on failure (#647)

M-C03: fix(pkg/core): disallow negative amount transitions (#645)

H-L01: fix(contracts/ChannelHub): add CH address to prevent val addition replay (#650)

M-H06(clearnode): restrict max channel challenge duration (#654)

M-M03(clearnode): revert EscrowLock on insufficient home user balance (#655)

M-M04(clearnode): support default signer even if it is not approved (#656)

M-M05(clearnode): require correct intent on FinalizeEscrowWithdrawal (#657)

M-M09: reject asset decimals exceeding its token's decimals (#659)

M-L01: return correct errors during state advancement validation (#660)

M-L03: limit number of related ids per session key state (#662)

M-I02: normalize input hex addresses (#663)

M-H01: feat(contracts/ChannelHub): restrict to one node (#649)

M-H07: fix(contracts/ChannelHub): emit stored, not arbitrary candidate state (#664)

M-I01: feat(contracts/ChannelHub): remove updateLastState flag (#665)

H-L06: fix(contracts/ChannelHub): remove payable from methods, clarify in create (#666)

H-L09: feat(contracts/ChannelEngine): add non-home migration version check (#669)

YNU-839: fix blockchain listener lifecycle (#658)

M-H09: use channel signer for all channel state node sigs (#667)

H-L07: fix(contracts/ChannelHub): restrict createChannel to non-existing channels (#668)

H-I02: docs(contracts): add a note that rebasing tokens are not supported (#670)

H-I03: fix(contracts/ChannelHub): use CIE pattern in depositToHub (#671)

M-H11: revert empty signatures on quorum verification (#672)

M-H11: reject issuance of receiver state during escrow ops (#674)

H-I01: docs(contracts): note that fee-on-transfer tokens are not supported (#675)

M-L04: docs: mention liquidity monitoring (#677)

fix(contracts/ChannelHub): fix initialize escrow deposit dos (#679)

M-H08: enforce strict transition ordering after MutualLock and EscrowLock (#680)

fix: run forge fmt (#681)

M-L05: docs(contracts): document native token deposits (#685)

fix(clearnode): resolve a set of audit findings (#686)

M-H13: feat(contract): add validateChallengeSignature, revert in SK validator (#688)
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