Skip to content

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

Merged
nksazonov merged 1 commit into
fix/audit-findingsfrom
fix/audit-m-m02
Apr 2, 2026
Merged

M-M02: fix(contracts): require zero allocs on close#643
nksazonov merged 1 commit into
fix/audit-findingsfrom
fix/audit-m-m02

Conversation

@nksazonov
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov commented Apr 1, 2026

Description

In cooperative path in closeChannel, the contract validates the submitted CLOSE state, computes transition effects, applies them, and then zeroes meta.lockedFunds. However, the engine does not enforce that the final allocations are actually paid out before the channel is deleted. _calculateCloseEffects only checks that userAllocation + nodeAllocation <= lockedFunds, that the resulting finalLockedFunds is non-negative, and that it is large enough for the special nodeAllocation handling.
In ChannelHub, cooperative close path does not automatically pay out the final allocations shown in the CLOSE state. The user only receives funds if userFundsDelta < 0. Consequently, if the signed CLOSE state keeps the same net flows as the previous state, both deltas can be zero even though the state still shows positive allocations. In that case, no funds are transferred during close, but _applyEffects still sets meta.lockedFunds to zero. The channel is removed, while the tokens remain stuck in the contract.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced validation for channel closure states to ensure both user and node allocations are properly zeroed during closure, preventing inconsistent fund handling.
  • Documentation

    • Updated channel closure specification to clarify that final payouts use net flow deltas for complete fund withdrawal, with channel status transitioning to CLOSED.
  • Tests

    • Added validation tests for closure intent handling with incorrect allocation scenarios.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

The pull request refactors channel closure validation and fund handling in the ChannelEngine and ChannelHub contracts. It replaces the previous allocation sum validation with a strict requirement that both user and node allocations must be zero during channel closure, and removes special-case fund-pushing logic from the state transition handler to rely on standard delta processing.

Changes

Cohort / File(s) Summary
Smart Contract Logic
contracts/src/ChannelEngine.sol, contracts/src/ChannelHub.sol
Changed CLOSE validation in ChannelEngine to enforce userAllocation == 0 and nodeAllocation == 0 instead of checking allocation sum bounds. Removed special-case handling in ChannelHub that directly pushed node allocation, now relying on existing net-flow delta processing.
Tests
contracts/test/ChannelEngine/ChannelEngine_validateTransition.t.sol
Added internal helper method for constructing operating contexts and introduced two new test cases validating that ChannelEngine correctly rejects CLOSE intents when user or node allocations are non-zero.
Documentation
protocol-description.md
Updated channel closure specification to clarify that CLOSE state must have both allocations at zero, with fund payouts handled through negative net flow deltas rather than direct allocation pushing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • dimast-x
  • ihsraham
  • philanton

Poem

🐰 When channels close, no tricks remain,
Just zeros where allocs did reign—
Delta flows now do the deed,
No special cases that we need!
Clean and pure, the closure's right,

🚥 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 'fix(contracts): require zero allocs on close' directly and specifically describes the main change: enforcing zero allocations in the CLOSE state to prevent funds from getting stuck in the contract.
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-m-m02

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 1, 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.

Comment thread contracts/src/ChannelEngine.sol
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.

🧹 Nitpick comments (1)
contracts/test/ChannelEngine/ChannelEngine_validateTransition.t.sol (1)

51-61: Good complementary test for node allocation.

This correctly validates that even when userAllocation == 0 passes the first check, a non-zero nodeAllocation still triggers the revert. The net flow setup (allocsSum == netFlowsSum) ensures the universal validation passes, isolating the CLOSE-specific check.

Consider adding a happy-path test that validates a successful CLOSE with zero allocations and appropriate negative deltas, ensuring the full transition completes without revert:

💡 Optional: Add happy-path test
function test_close_success_zeroAllocations() public {
    ChannelEngine.TransitionContext memory ctx = _operatingCtx(1000);

    // User withdraws all funds: userAllocation -> 0, userNetFlow -> 0
    // userNfDelta = 0 - 1000 = -1000 (push 1000 to user)
    State memory candidate =
        TestUtils.nextState(ctx.prevState, StateIntent.CLOSE, [uint256(0), uint256(0)], [int256(0), int256(0)]);

    ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
    
    assertEq(effects.userFundsDelta, -1000);
    assertEq(effects.nodeFundsDelta, 0);
    assertTrue(effects.closeChannel);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/test/ChannelEngine/ChannelEngine_validateTransition.t.sol` around
lines 51 - 61, Add a happy-path unit test named e.g.
test_close_success_zeroAllocations that uses TransitionContext ctx =
_operatingCtx(1000), builds a candidate State via TestUtils.nextState with
StateIntent.CLOSE and zero allocations/deltas (both user and node zero), calls
ChannelEngine.validateTransition(ctx, candidate) to get
ChannelEngine.TransitionEffects, and asserts effects.userFundsDelta equals
-1000, effects.nodeFundsDelta equals 0, and effects.closeChannel is true;
reference the existing helpers TransitionContext, TestUtils.nextState,
ChannelEngine.validateTransition, and the TransitionEffects fields to locate
where to add the test.
🤖 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/test/ChannelEngine/ChannelEngine_validateTransition.t.sol`:
- Around line 51-61: Add a happy-path unit test named e.g.
test_close_success_zeroAllocations that uses TransitionContext ctx =
_operatingCtx(1000), builds a candidate State via TestUtils.nextState with
StateIntent.CLOSE and zero allocations/deltas (both user and node zero), calls
ChannelEngine.validateTransition(ctx, candidate) to get
ChannelEngine.TransitionEffects, and asserts effects.userFundsDelta equals
-1000, effects.nodeFundsDelta equals 0, and effects.closeChannel is true;
reference the existing helpers TransitionContext, TestUtils.nextState,
ChannelEngine.validateTransition, and the TransitionEffects fields to locate
where to add the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9eef6887-e8d3-4958-9d6c-acccc081e0d2

📥 Commits

Reviewing files that changed from the base of the PR and between 8151e30 and 4558e39.

📒 Files selected for processing (4)
  • contracts/src/ChannelEngine.sol
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelEngine/ChannelEngine_validateTransition.t.sol
  • protocol-description.md
💤 Files with no reviewable changes (1)
  • contracts/src/ChannelHub.sol

@nksazonov nksazonov merged commit b9f0a18 into fix/audit-findings Apr 2, 2026
3 checks passed
@nksazonov nksazonov deleted the fix/audit-m-m02 branch April 2, 2026 12:04
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