Skip to content
On this page

L1-4: Custom Stream (Advanced Streaming)

1. Problem

Build a more advanced streaming payment system on top of ETH Streaming, supporting multiple recipients, multi-stream management, stream cancellation, and other features.

2. Why

The basic ETH Streaming only supports a single ETH stream. Real-world applications require:

  • Managing multiple concurrent streams
  • Supporting ERC20 token streams (not just ETH)
  • Stream lifecycle management (create, cancel, fund recovery)

3. Solution

Architecture Design

solidity
struct Stream {
    address token;              // ERC20 token address (address(0) = ETH)
    uint256 cap;                // Maximum withdrawable amount per cycle
    uint256 unlockTime;         // Full unlock cycle duration
    uint256 timeOfLastWithdrawal;
    uint256 remainingBalance;   // Total remaining budget for this stream
}

Multi-Stream Management

  • streamId identifies each stream
  • streamId → recipient → Stream two-level mapping
  • Each stream independently tracks budget and unlock state

Stream Lifecycle

  1. createStream(token, totalBudget) — create and fund
  2. addRecipient(streamId, recipient, cap, unlockTime) — add recipient
  3. withdraw(streamId, amount) — recipient withdraws
  4. cancelStream(streamId) — owner cancels stream

4. Pitfalls Encountered

4.1 Confusing remainingBalance with cap

cap is the maximum withdrawable amount per unlock cycle (recovers over time), while remainingBalance is the recipient's total budget cap (does not recover).

4.2 Withdrawals not blocked after cancellation

If the activeStreams flag is not cleared after stream cancellation, recipients could still withdraw.

5. Why the Pitfalls Happen

5.1

cap operates on the time-based unlock model (same as L1-3), while remainingBalance is a fixed budget ceiling. The two must be handled separately: in withdraw, first check unlocked (time dimension), then check remainingBalance (budget dimension).

5.2

After stream cancellation modifies internal state, the withdraw entry point must check activeStreams[streamId].

6. How to Resolve the Pitfalls

solidity
if (amount > unlocked) revert InsufficientUnlockedBalance();
if (amount > stream.remainingBalance) revert InsufficientStreamBalance();

7. Technical Highlights

PointDescription
Multi-stream architecturestreamId + two-level mapping
Budget vs rateremainingBalance (total) vs cap (rate)
Stream lifecyclecreate → fund → addRecipient → withdraw → cancel
ExtensibilityEach stream can configure an independent unlockTime

Built with AiAda