|
| 1 | +# `stream_contract` |
| 2 | + |
| 3 | +Soroban smart contract for time-based token streaming with optional protocol fees. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +`stream_contract` lets a sender deposit tokens into a stream that accrues linearly to a recipient over time. |
| 8 | +The contract supports stream creation, top-ups, withdrawals, cancellation, pause/resume controls, and protocol fee administration. |
| 9 | + |
| 10 | +- Fee cap: `MAX_FEE_RATE_BPS = 1000` (10%) |
| 11 | +- Fee unit: basis points (`bps`), where `100 bps = 1%` |
| 12 | +- Fee collection points: `create_stream`, `top_up_stream` |
| 13 | + |
| 14 | +## Public API |
| 15 | + |
| 16 | +All entrypoints are in `src/lib.rs` under `impl StreamContract`. |
| 17 | + |
| 18 | +### Protocol administration |
| 19 | + |
| 20 | +| Function | Purpose | |
| 21 | +|---|---| |
| 22 | +| `initialize(env, admin, treasury, fee_rate_bps)` | One-time protocol config setup | |
| 23 | +| `update_fee_config(env, admin, treasury, fee_rate_bps)` | Update treasury and/or fee rate (admin-only) | |
| 24 | +| `transfer_admin(env, current_admin, new_admin)` | Transfer admin role | |
| 25 | +| `get_fee_config(env)` | Read current fee config (`Option<ProtocolConfig>`) | |
| 26 | + |
| 27 | +### Stream lifecycle |
| 28 | + |
| 29 | +| Function | Purpose | |
| 30 | +|---|---| |
| 31 | +| `create_stream(env, sender, recipient, token_address, amount, duration)` | Create stream from deposited funds | |
| 32 | +| `top_up_stream(env, sender, stream_id, amount)` | Add more funds to an active stream | |
| 33 | +| `withdraw(env, recipient, stream_id)` | Recipient withdraws currently claimable amount | |
| 34 | +| `cancel_stream(env, sender, stream_id)` | Sender cancels stream and receives remaining balance | |
| 35 | +| `pause_stream(env, sender, stream_id)` | Freeze accrual on an active stream | |
| 36 | +| `resume_stream(env, sender, stream_id)` | Resume accrual and recompute stream end time | |
| 37 | + |
| 38 | +### Read-only queries |
| 39 | + |
| 40 | +| Function | Purpose | |
| 41 | +|---|---| |
| 42 | +| `get_stream(env, stream_id)` | Return full stream record (`Option<Stream>`) | |
| 43 | +| `is_stream_completed(env, stream_id)` | Return completion status | |
| 44 | +| `get_claimable_amount(env, stream_id)` | Compute current claimable amount without state changes | |
| 45 | + |
| 46 | +## Fee and treasury model |
| 47 | + |
| 48 | +Protocol fee config is optional; if not initialized, fee collection is a no-op. |
| 49 | + |
| 50 | +When initialized and `fee_rate_bps > 0`: |
| 51 | + |
| 52 | +- Fee formula: `fee = amount * fee_rate_bps / 10_000` |
| 53 | +- Net credited to stream: `amount - fee` |
| 54 | +- Fee recipient: configured `treasury` address |
| 55 | +- Fee event: `fee_collected` is emitted only when `fee > 0` |
| 56 | + |
| 57 | +### Rounding behavior |
| 58 | + |
| 59 | +Fee math uses integer division. For tiny amounts, fee can round down to zero. |
| 60 | + |
| 61 | +Example: |
| 62 | +- `amount = 1` |
| 63 | +- `fee_rate_bps = 200` (2%) |
| 64 | +- `fee = 1 * 200 / 10_000 = 0` |
| 65 | + |
| 66 | +In this case: |
| 67 | +- no transfer to treasury occurs, |
| 68 | +- no `fee_collected` event is emitted, |
| 69 | +- full amount is credited to the stream. |
| 70 | + |
| 71 | +## Event topics |
| 72 | + |
| 73 | +Events are emitted with the following topics (see `src/events.rs`): |
| 74 | + |
| 75 | +| Event struct | Topic | |
| 76 | +|---|---| |
| 77 | +| `InitializedEvent` | `("initialized",)` | |
| 78 | +| `FeeConfigUpdatedEvent` | `("fee_config_updated",)` | |
| 79 | +| `AdminTransferredEvent` | `("admin_transferred",)` | |
| 80 | +| `StreamCreatedEvent` | `("stream_created", stream_id)` | |
| 81 | +| `StreamToppedUpEvent` | `("stream_topped_up", stream_id)` | |
| 82 | +| `TokensWithdrawnEvent` | `("tokens_withdrawn", stream_id)` | |
| 83 | +| `StreamCancelledEvent` | `("stream_cancelled", stream_id)` | |
| 84 | +| `StreamPausedEvent` | `("stream_paused", stream_id)` | |
| 85 | +| `StreamResumedEvent` | `("stream_resumed", stream_id)` | |
| 86 | +| `StreamCompletedEvent` | `("stream_completed", stream_id)` | |
| 87 | +| `FeeCollectedEvent` | `("fee_collected", stream_id)` | |
| 88 | + |
| 89 | +## `StreamError` reference |
| 90 | + |
| 91 | +Error codes from `src/errors.rs`: |
| 92 | + |
| 93 | +| Code | Variant | Meaning | |
| 94 | +|---:|---|---| |
| 95 | +| 1 | `InvalidAmount` | Amount is zero/negative/out of range | |
| 96 | +| 2 | `StreamNotFound` | Stream ID does not exist | |
| 97 | +| 3 | `Unauthorized` | Caller not authorized for stream action | |
| 98 | +| 4 | `StreamInactive` | Operation requires an active stream | |
| 99 | +| 5 | `AlreadyInitialized` | `initialize` called more than once | |
| 100 | +| 6 | `NotAdmin` | Caller is not protocol admin | |
| 101 | +| 7 | `InvalidFeeRate` | Fee exceeds `MAX_FEE_RATE_BPS` | |
| 102 | +| 8 | `NotInitialized` | Protocol config not initialized | |
| 103 | +| 9 | `InvalidDuration` | Duration is zero | |
| 104 | +| 10 | `InvalidTokenAddress` | Token address is not a token contract | |
| 105 | +| 11 | `InvalidRate` | `amount / duration` rounds to zero | |
| 106 | + |
| 107 | +## Typical flow |
| 108 | + |
| 109 | +1. Admin calls `initialize` with treasury and fee rate. |
| 110 | +2. Sender calls `create_stream`. |
| 111 | +3. Sender may call `top_up_stream`, `pause_stream`, `resume_stream`, or `cancel_stream`. |
| 112 | +4. Recipient calls `withdraw` over time until fully drained. |
| 113 | +5. Final withdrawal emits `stream_completed`. |
0 commit comments