Skip to content

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

Merged
nksazonov merged 1 commit into
fix/audit-findingsfrom
fix/audit-h-m01
Mar 30, 2026
Merged

H-M01: fix(contracts/ChannelHub): remove transferred amount check#636
nksazonov merged 1 commit into
fix/audit-findingsfrom
fix/audit-h-m01

Conversation

@nksazonov
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov commented Mar 30, 2026

Description

The _pushFunds function uses a balance-checking pattern to verify ERC20 transfer success by comparing balances before and after the transfer. However, when success == true but the balance check fails (balanceAfter != balanceBefore - amount), the protocol incorrectly credits the full amount to _reclaims even though the tokens have already been transferred to the recipient. This creates a double-spend vulnerability where the recipient receives tokens twice: once from the actual transfer and once from claiming the reclaim. The vulnerability is triggered when the contract's balance increases during the transfer operation, which occurs with ERC777 tokens when the recipient's tokensReceived hook donates tokens back to ChannelHub. The protocol explicitly supports ERC777 tokens (evidenced by the L150 comment about 'ERC777 hooks' in gas limit calculation), making this an intended use case. The vulnerability affects all operations calling _pushFunds(...): withdrawFromVault(..), closeChannel(..), finalizeEscrowDeposit(..), finalizeEscrowWithdrawal(..), and challenge processing.

Impact

A malicious node operator can deploy a contract with a malicious tokensReceived hook that donates tokens back to ChannelHub during withdrawals. This causes the balance check to fail even though the transfer succeeded, resulting in the full amount being credited to reclaims. The attacker can then claim these funds again via claimFunds, effectively receiving 2x the withdrawn amount while only 1x is deducted from their vault accounting. By repeating this attack, the attacker can drain their entire vault balance plus an equal amount from the protocol's reserves, creating an insolvency situation where other users cannot withdraw their full balances. The cost is minimal (1-2 wei per withdrawal) and the profit is 100% of the withdrawn amount. Since the ChannelHub holds tokens for all nodes in a shared balance, the accounting shortfall created by this exploit affects all other users of the same token, effectively allowing the attacker to steal funds that belong to other nodes.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced ERC20 transfer failure detection to properly handle edge-case token behaviors that could previously cause incorrect transaction processing, improving protocol robustness.
  • Documentation

    • Updated security guidelines to detail ERC20 transfer failure mechanisms and potential attack vectors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cf30001-87cf-4462-ba26-bc13e02cbbf9

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0a27e and e368985.

📒 Files selected for processing (4)
  • contracts/SECURITY.md
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_pushFunds.t.sol
  • contracts/test/mocks/DonatingERC20.sol

📝 Walkthrough

Walkthrough

The PR replaces ERC20 transfer failure detection in ChannelHub's _pushFunds from balance-delta checking to return-value checking via a new _trySafeTransfer helper. Documentation clarifies two failure vectors: gas depletion and donation-back double-spend. New tests and a DonatingERC20 mock validate the updated approach.

Changes

Cohort / File(s) Summary
Transfer Failure Detection Implementation
contracts/src/ChannelHub.sol
Replaced balance-delta verification with return-value-based detection via new _trySafeTransfer(...) internal function. On transfer failure, credits _reclaims[to][token] and emits TransferFailed event. Handles empty returns, boolean-encoded returns, and non-standard short return data according to return-data semantics.
Test Coverage & Mocks
contracts/test/ChannelHub_pushFunds.t.sol, contracts/test/mocks/DonatingERC20.sol
Added test for ERC20 returning false on transfer failure (test_accumulatesReclaims_whenERC20ReturnsFalse) and test for donation-back edge case (test_succeeds_whenERC777DonatesBack). Introduced DonatingERC20 mock contract that donates tokens back to ChannelHub during transfers to validate no false reclaim credit occurs.
Security Documentation
contracts/SECURITY.md
Expanded ERC777 gas depletion attack documentation with explicit description of donation-back double-spend vector and clarified that protocol uses return-value checking (not balance-delta checking) for transfer success determination.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant ChannelHub
    participant Token
    
    rect rgba(100, 149, 237, 0.5)
    note over Caller,Token: New Return-Value-Based Transfer Failure Detection
    Caller->>ChannelHub: _pushFunds(to, token, amount)
    ChannelHub->>ChannelHub: _trySafeTransfer(token, to, amount)
    ChannelHub->>Token: transfer{gas: TRANSFER_GAS_LIMIT}(to, amount)
    
    alt Transfer Success (returns true)
        Token-->>ChannelHub: return true
        ChannelHub-->>Caller: Proceed (no reclaim credit)
    else Transfer Failure (returns false or reverts)
        Token-->>ChannelHub: return false or revert
        ChannelHub->>ChannelHub: _reclaims[to][token] += amount
        ChannelHub->>ChannelHub: emit TransferFailed(...)
        ChannelHub-->>Caller: Failure handled, reclaim credited
    end
    end
    
    rect rgba(255, 165, 0, 0.5)
    note over Caller,Token: Donation-Back Edge Case (Handled Correctly)
    Caller->>ChannelHub: _pushFunds(to, token, amount)
    ChannelHub->>Token: transfer{gas: TRANSFER_GAS_LIMIT}(to, amount)
    Token->>Token: Execute transfer to recipient
    Token->>Token: Donate tokens back to ChannelHub
    Token-->>ChannelHub: return true
    ChannelHub-->>Caller: Transfer succeeds (no false reclaim)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ihsraham
  • philanton
  • dimast-x

Poem

🐰 A hop and a bound, returns now reign supreme,
No balance-checks needed, just return values gleam—
Donations may dance back, but we're wise to the trick,
Safe transfers at last, swift and slick!
✨🥕

🚥 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 and specifically identifies the main change: removing the transferred amount check from ChannelHub's _pushFunds function to fix a vulnerability.
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-m01

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 Mar 30, 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.

@nksazonov nksazonov merged commit 7177e1d into fix/audit-findings Mar 30, 2026
3 checks passed
@nksazonov nksazonov deleted the fix/audit-h-m01 branch March 30, 2026 11:48
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