Skip to content

fix(contracts): enforce CEI in withdraw and cancel_stream to close reentrancy surface#947

Open
patopatrish wants to merge 1 commit into
LabsCrypt:mainfrom
patopatrish:fix/789-cei-reentrancy-withdraw-cancel
Open

fix(contracts): enforce CEI in withdraw and cancel_stream to close reentrancy surface#947
patopatrish wants to merge 1 commit into
LabsCrypt:mainfrom
patopatrish:fix/789-cei-reentrancy-withdraw-cancel

Conversation

@patopatrish

Copy link
Copy Markdown

Closes #789

Problem

withdraw and cancel_stream both transferred tokens before committing updated stream state to storage, violating the Checks-Effects-Interactions (CEI) pattern.

token_address is a caller-supplied contract. A token with a transfer hook could re-enter withdraw or cancel_stream while persistent storage still reflected the pre-update state (stale withdrawn_amount, is_active = true), enabling a double payout.

Affected code paths

Function Bug
transfer_and_update_stream (called by withdraw) token_client.transfer fired before updating withdrawn_amount / is_active, and save_stream ran even later, after the helper returned
cancel_stream Both recipient and sender token_client.transfer calls fired before save_stream

Fix

apply_withdrawal (replaces transfer_and_update_stream) — new helper that enforces CEI:

  1. Effects: increment withdrawn_amount, set last_update_time, flip is_active/status if fully drained
  2. Persist: save_stream — state is now durable before any external call
  3. Interaction: token_client.transfer

withdraw — delegates to apply_withdrawal; the standalone save_stream call is removed (now inside the helper).

cancel_stream — restructured so all state mutations + save_stream precede both token transfers (recipient payout and sender refund).

Regression tests added

  • test_withdraw_state_committed_before_transfer_prevents_double_payout — after a successful withdraw, a second call at the same ledger timestamp (simulating a re-entrant hook) returns InvalidAmount because committed state already reflects the withdrawal.
  • test_cancel_state_committed_before_transfers_prevents_double_cancel — after cancel_stream, a second cancel attempt returns StreamInactive because the stream was marked inactive before either transfer fired.

Files changed

  • contracts/stream_contract/src/lib.rs
  • contracts/stream_contract/src/test.rs

…e reentrancy surface (LabsCrypt#789)

Both `withdraw` and `cancel_stream` previously executed token transfers before
persisting stream state, violating Checks-Effects-Interactions. A token contract
with a transfer hook could re-enter either function while storage still held the
pre-update state, enabling a double payout.

Changes:
- Rename `transfer_and_update_stream` → `apply_withdrawal`, which now follows
  CEI: update stream fields → save_stream (effects committed) → token transfer.
- `withdraw` delegates to `apply_withdrawal`; the separate `save_stream` call is
  removed since persistence is now handled inside the helper.
- `cancel_stream` is restructured so all state mutations and `save_stream` happen
  before either token transfer (recipient payout + sender refund).
- Two regression tests added: one for `withdraw` and one for `cancel_stream`,
  each asserting that a second call at the same timestamp fails because committed
  state already reflects the first operation.

Closes LabsCrypt#789

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

[Contracts] withdraw/cancel transfer tokens before persisting stream state (CEI violation / reentrancy surface)

1 participant