Appearance
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
streamIdidentifies each streamstreamId → recipient → Streamtwo-level mapping- Each stream independently tracks budget and unlock state
Stream Lifecycle
createStream(token, totalBudget)— create and fundaddRecipient(streamId, recipient, cap, unlockTime)— add recipientwithdraw(streamId, amount)— recipient withdrawscancelStream(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
| Point | Description |
|---|---|
| Multi-stream architecture | streamId + two-level mapping |
| Budget vs rate | remainingBalance (total) vs cap (rate) |
| Stream lifecycle | create → fund → addRecipient → withdraw → cancel |
| Extensibility | Each stream can configure an independent unlockTime |