H-L02: fix(contracts/ChannelHub): revert on withdrawFromVault failure#651
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR introduces a dual-function approach to fund transfers in ChannelHub: a reverting Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
_nonRevertingPushFundshas 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 asboolusingabi.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
📒 Files selected for processing (5)
contracts/src/ChannelHub.solcontracts/test/ChannelHub_Base.t.solcontracts/test/ChannelHub_IVault.t.solcontracts/test/ChannelHub_nonRevertingPushFunds.t.solcontracts/test/TestChannelHub.sol
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)
Description
A node can permanently lock their own vault funds by calling
withdrawFromVault(..)with a contract address that:claimFunds(..)to recover the funds from reclaimsAt 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, ifcontractAddressis a contract without the capability to callclaimFunds(..)(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
Tests