Skip to content

Latest commit

 

History

History
318 lines (217 loc) · 21.9 KB

File metadata and controls

318 lines (217 loc) · 21.9 KB

SafeStream — Comprehensive Project Plan

Protocol: Soroban Smart Contracts (Stellar Network) Language: Rust (no_std, compiled to WebAssembly) Version: 0.1.0 Author: Bojest001 — belovedj66@gmail.com Repository: https://github.com/Bojest001/Soroban-SafeStream License: MIT Wave: Drips Wave — High Complexity Tier (200 Points)


1. Executive Summary

SafeStream is a production-grade, linear token streaming protocol built on Soroban smart contracts for the Stellar network. It solves a fundamental problem in decentralised finance and decentralised autonomous organisations: how do you pay contributors, vest tokens for founders, or fund recurring public-goods initiatives in a trustless, programmable, and time-based way without relying on centralised payroll systems or manual transfers?

SafeStream answers this by locking any Stellar Asset Contract (SAC) compatible token — including USDC, XLM, or any custom asset — into a tamper-proof on-chain escrow and releasing it linearly over a defined time window to a designated receiver. The receiver can withdraw their vested portion at any point. The sender retains the right to cancel early with a pro-rata refund. A delegate system allows receivers to automate withdrawals through trusted third-party accounts or keeper bots. An admin governance layer provides emergency controls with a transferable admin role suitable for eventual DAO governance.

The protocol is directly aligned with the ethos of the Drips platform — programmable, recurring, trust-minimised funding — and demonstrates mastery of Protocol 26 Time-To-Live (TTL) state management, ensuring that long-running streams spanning weeks or months are never silently evicted from the Stellar ledger due to state rent limits.


2. Problem Statement

Traditional token distribution methods in Web3 are blunt instruments. Sending a lump sum requires trust. On Stellar specifically, there has been no native protocol for programmatic, time-based token streaming — organisations either resort to manual monthly transfers, trusted custodians, or multi-signature wallets requiring human approval at every step.

This creates concrete problems. For contributors and employees there is no guarantee of payment continuity — if a DAO treasury runs dry or a company disbands, vested but unpaid tokens may be lost. For DAOs and organisations, manual transfers are operationally expensive, prone to human error, and require active management. For public-goods funding, one-time grants are ineffective — recurring, programmable funding aligned with actual work output is what the ecosystem needs. For token vesting, founders and early contributors need long-duration schedules of one to four years, and any smart contract managing these must guarantee that state is never lost from the ledger even if months pass between interactions.

This last concern — the Protocol 26 TTL problem — is the most technically demanding, and SafeStream addresses it directly and comprehensively.


3. Project Goals

# Goal Priority Status
G1 Linear, block-by-block token streaming with correct vesting math Critical ✅ Complete
G2 Protocol 26 TTL safety — no stream ever evicted from the ledger Critical ✅ Complete
G3 Typed error codes for all failure modes — no raw panics in logic Critical ✅ Complete
G4 Delegate withdrawal system for automation and keeper bots High ✅ Complete
G5 Emergency pause with transferable admin governance High ✅ Complete
G6 Pro-rata cancellation with fair refund to sender High ✅ Complete
G7 Comprehensive 50-test integration suite across 9 test groups High ✅ Complete
G8 Build, test, deploy, and invoke scripts for developer experience Medium ✅ Complete
G9 Clean, professional public GitHub repository with full docs Medium ✅ Complete
G10 Testnet deployment and smoke testing Future ⏳ Pending
G11 Third-party security audit before mainnet TVL Future ⏳ Pending
G12 Mainnet production deployment Future ⏳ Pending

4. Architecture Overview

SafeStream is structured as a single Soroban smart contract compiled to a WASM binary. The source code is split into four focused modules plus a test module, each with a single well-defined responsibility.

soroban-safestream/
├── Cargo.toml                  ← Package manifest, soroban-sdk 22.0.0, release profile
├── .cargo/config.toml          ← Default wasm32-unknown-unknown build target
├── .gitignore                  ← Excludes target/, .env, secrets, .contract_id
├── LICENSE                     ← MIT
├── README.md                   ← Full user-facing documentation
├── plan.md                     ← This project plan
├── src/
│   ├── lib.rs                  ← 15 public contract entry-points
│   ├── stream.rs               ← Stream struct, vesting math, storage helpers, TTL
│   ├── errors.rs               ← 10-variant StreamError enum (contracterror)
│   ├── events.rs               ← 8 on-chain event emitters
│   └── test.rs                 ← 50 integration tests across 9 groups
└── scripts/
    ├── build.sh                ← Compile + optimise WASM binary
    ├── deploy.sh               ← Upload WASM + deploy contract instance
    ├── test.sh                 ← Run full test suite
    └── invoke_example.sh       ← End-to-end CLI walkthrough

lib.rs is the contract's public face. It defines the SafeStream struct and all 15 publicly callable entry-points. Its responsibilities are strictly limited to validating inputs, enforcing authentication, and orchestrating the sequence of operations (check → effect → transfer → emit event).

stream.rs is the data and storage layer. It defines the Stream struct, the DataKey enum covering all five storage key variants, and every helper function that reads from or writes to Soroban persistent and instance storage. All vesting arithmetic lives here as methods on Stream, making them independently unit-testable without deploying a contract.

errors.rs defines a single StreamError enum annotated with #[contracterror]. Every variant maps to a stable u32 error code on the Stellar ledger, giving clients, wallets, and indexers structured machine-readable errors rather than raw panics.

events.rs contains eight dedicated event-emitting functions, one per contract state transition. Every event is emitted after state mutations and token transfers have fully settled, following the Checks-Effects-Interactions (CEI) ordering pattern.

test.rs is the 50-test integration suite using soroban-sdk testutils to deploy a mock contract, register a mock token, and simulate time progression.


5. Contract Entry-Points

SafeStream exposes 15 public entry-points organised into four functional groups.

Initialisation

initialize(admin) — Called exactly once immediately after deployment, ideally in the same transaction to prevent front-running. Stores the admin address in instance storage and panics with "already initialised" on subsequent calls.

Stream Lifecycle

create_stream(sender, receiver, token, amount, start_time, end_time) → Result<u64> — The sender must sign. Validates that end_time > start_time and amount > 0 (explicitly rejecting negative i128 values). Atomically increments the global counter, persists a new Stream struct with full TTL management, transfers tokens from sender to the contract escrow, and emits a created event. Returns the unique stream ID.

withdraw(stream_id) → Result<i128> — Only the receiver may call this. Computes the vested-but-unwithdrawn balance at the current ledger timestamp and transfers it. If this drains the stream completely, both the stream entry and any delegate entry are removed from storage immediately. Emits either a withdraw event (partial) or a complete event (final).

withdraw_on_behalf(stream_id, caller) → Result<i128> — Allows an approved delegate to trigger a withdrawal on behalf of the receiver. The caller must be the approved delegate and must sign. Tokens always go to the receiver — the delegate never receives funds. This enables keeper bots and automated systems to service streams.

cancel_stream(stream_id) → Result<()> — Only the sender may cancel, and only before end_time. Tokens vested but not withdrawn go to the receiver; unvested tokens are refunded to the sender. Both entries are removed from storage. Emits a cancel event with the exact split amounts.

Delegate Management

set_delegate(stream_id, delegate) → Result<()> — Only the receiver may set a delegate. The delegate cannot be the receiver or sender (both rejected with InvalidDelegate). Replaces any existing delegate.

revoke_delegate(stream_id) → Result<()> — Removes the approved delegate. Only the receiver may revoke.

Admin and Governance

pause(admin) → Result<()> — Immediately halts all state-mutating calls. Read-only calls remain available. Only the current admin may pause.

unpause(admin) → Result<()> — Resumes normal operation. Admin only.

transfer_admin(admin, new_admin) → Result<()> — Transfers the admin role. The intended upgrade path before mainnet is to point admin at a multisig or DAO governance contract, removing the single point of failure.

Read-Only Queries (always available, even when paused)

Function Returns Description
get_stream(stream_id) Result<Stream> Full stream state
withdrawable(stream_id) Result<i128> Tokens withdrawable right now
stream_count() u64 Total streams ever created
get_delegate(stream_id) Option<Address> Approved delegate if set
paused() bool Current pause state
admin() Address Current admin address

6. Storage Design and TTL Management

SafeStream uses both Soroban storage tiers strategically.

Instance storage holds three global values that are accessed on nearly every call: StreamCount (u64 counter), Admin (Address), and Paused (bool). Instance storage is cheap and lives as long as the contract instance.

Persistent storage holds per-stream entries subject to state-rent: Stream(id) containing the full Stream struct, and Delegate(id) containing the approved delegate address if set.

The Protocol 26 TTL constants are:

pub const PERSISTENT_BUMP_AMOUNT: u32 = 518_400;       // ~30 days at 5s/ledger
pub const PERSISTENT_LIFETIME_THRESHOLD: u32 = 172_800; // ~10 days at 5s/ledger

The rule applied throughout the codebase is: on every persistent storage read or write, call extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT). If the entry's remaining lifetime drops below 10 days, it is bumped back to 30 days. This means even a stream that goes untouched for 20 days is automatically refreshed the next time any party interacts with it — withdrawing, querying, or cancelling.

When a stream completes or is cancelled, remove_stream() is called which internally calls env.storage().persistent().remove() on both the Stream(id) and Delegate(id) keys. This stops all future rent charges immediately, making the contract economically self-cleaning. There is no accumulation of dead state paying rent indefinitely.


7. Vesting Formula

The core economic primitive is linear vesting:

For start_time < now < end_time:
    vested = total_amount × (now − start_time) / (end_time − start_time)

For now ≤ start_time:   vested = 0
For now ≥ end_time:     vested = total_amount

withdrawable = vested − withdrawn

All arithmetic uses i128. The release profile sets overflow-checks = true, meaning any arithmetic overflow panics rather than silently wraps — a critical safety property for financial contracts. The formula is implemented as methods on the Stream struct, making it independently unit-testable. The boundary test suite verifies: zero is returned exactly at start_time, total_amount is returned at and after end_time, integer division truncation is correct for single-token streams, no overflow occurs for amounts near i128::MAX / 4, and a 10-year stream correctly computes 1% vesting at the 1% elapsed mark.


8. Error Codes

All entry-points that can fail return Result<T, StreamError>. The enum is annotated with #[contracterror] and #[repr(u32)] so each variant serialises as a stable unsigned 32-bit integer on the ledger.

Code Variant When Returned
1 InvalidTimeRange end_timestart_time
2 InvalidAmount amount ≤ 0 (zero or negative)
3 StreamNotFound Stream ID absent from storage
4 NothingToWithdraw Vested minus withdrawn is zero
5 Unauthorized Caller is not the approved delegate
6 StreamAlreadyComplete Stream is fully drained
7 CannotCancelAfterEnd Cancel attempted after end_time
8 ContractPaused Any mutating call while paused
9 NotAdmin Admin-only call by non-admin
10 InvalidDelegate Delegate is receiver or sender

Every error code is exercised by at least one test in the suite.


9. On-Chain Events

Eight structured on-chain events cover every state transition. Events are emitted after all state mutations and token transfers have settled (CEI pattern).

Event Topics Trigger Data
("created", stream_id) create_stream sender, receiver, token, amount, start_time, end_time
("withdraw", stream_id) Partial withdrawal receiver, caller, amount_withdrawn, total_withdrawn
("complete", stream_id) Final withdrawal receiver
("cancel", stream_id) cancel_stream sender, returned_to_sender, released_to_receiver
("delegate", stream_id) set_delegate receiver, delegate
("paused",) pause admin
("unpaused",) unpause admin
("newadmin",) transfer_admin old_admin, new_admin

The withdraw and complete events are distinct: withdraw signals the stream is still alive, while complete signals the stream has been fully drained and its storage entry removed from the ledger.


10. Security Analysis

Authentication and Authorisation — Every state-mutating entry-point enforces require_auth() for the appropriate party before performing any action. Senders control creation and cancellation. Receivers control withdrawal and delegation. Only the admin controls pause and governance. Auth failures cause transaction-level panics — they cannot be caught by the contract.

Integer Overflow — All token arithmetic uses i128 with overflow-checks = true in the release profile. Any overflow panics immediately rather than silently wrapping.

Re-entrancy — Soroban contracts execute in a single-threaded, deterministic environment per transaction. There is no asynchronous execution and no callbacks, making re-entrancy architecturally impossible.

State Eviction — The Protocol 26 TTL extend_ttl on every persistent storage access prevents long-running streams from being evicted. Completed and cancelled streams self-remove, preventing dead state from accumulating rent.

Self-Delegation Prevention — A receiver cannot delegate to themselves (meaningless) or to the sender (role confusion). Both are rejected with InvalidDelegate, enforcing clean role separation.

Front-Running on Initialisation — The deployment guide requires initialize to be called in the same transaction as deploy, making it economically impractical for a malicious actor to front-run the admin role claim.

Negative Amount Rejection — The explicit amount <= 0 check covers both zero and all negative i128 values, preventing streams that would pay the wrong direction.

Orphaned Storageremove_stream internally removes both the Stream(id) and Delegate(id) keys, preventing orphaned delegate entries from accumulating rent after their parent stream is gone.


11. Test Suite

The 50-test suite uses the soroban-sdk testutils harness. A shared TestEnv struct is set up per test: it registers a fresh contract, registers a mock SAC token, mints 10,000 tokens to the sender, and calls initialize. A create_default_stream helper creates a 1,000-token stream over 1,000 seconds starting 100 seconds from now.

Group Tests Coverage
Initialize 2 One-time setup, re-init panic
create_stream 6 Success, sequential IDs, invalid range, zero amount, negative amount, paused
withdraw 6 Before start, midpoint 50%, full drain, accumulation, not found, paused
Delegate / withdraw_on_behalf 9 Set, get, successful, no delegate, wrong delegate, revoke, completion cleanup, paused, self-delegation
cancel_stream 6 Before start full refund, mid-stream split, after partial withdrawal, after end_time, delegate cleanup, paused
Admin / Pause 6 Pause/unpause cycle, non-admin rejection × 2, transfer admin, non-admin transfer rejected, read-only while paused
Storage hygiene 5 Correct data, zero before start, resets after withdrawal, removed on completion, removed on cancel
Vesting boundary math 7 Zero at start, full at end, midpoint, 1s duration, single token, large i128, 10-year duration
Event emission 8 All 8 event topic tuples verified by assertion

Every test makes concrete numerical assertions on token balances, stream state fields, or error types.


12. Developer Experience

Buildbash scripts/build.sh runs cargo build --target wasm32-unknown-unknown --release then stellar contract optimize. The release profile uses opt-level = "z", lto = true, codegen-units = 1, and panic = "abort".

Testbash scripts/test.sh runs the full 50-test suite in-process. No network connection required. Supports an optional filter argument: bash scripts/test.sh withdraw.

Deploybash scripts/deploy.sh uploads the WASM binary and creates a contract instance on testnet (or mainnet with NETWORK=mainnet). The contract ID is saved to .contract_id.

Invokebash scripts/invoke_example.sh walks through the full lifecycle using the Stellar CLI: create_stream, get_stream, withdrawable, and withdraw.


13. Deployment Checklist

  • Install Rust and add wasm32-unknown-unknown target
  • Install Stellar CLI: cargo install --locked stellar-cli
  • Generate deployer identity: stellar keys generate --global deployer --network testnet
  • Fund on testnet: stellar keys fund deployer --network testnet
  • Run bash scripts/build.sh — confirm WASM produced without errors
  • Run bash scripts/test.sh — confirm all 50 tests pass
  • Deploy to testnet: bash scripts/deploy.sh
  • Call initialize(admin) in the same transaction as deploy
  • Smoke test all entry-points on testnet with real SAC token addresses
  • Verify withdrawable returns correct vested amounts as time advances
  • Test pause and confirm mutating calls are blocked
  • Transfer admin to a multisig with at least 2-of-3 signers
  • Commission a third-party security audit from a Soroban-specialist firm
  • Address all audit findings of medium severity or above
  • Deploy to mainnet: NETWORK=mainnet bash scripts/deploy.sh
  • Verify contract ID on Stellar Expert or equivalent explorer
  • Publish contract ID alongside the audit report

14. Roadmap

Phase Milestone Status
1 Core streaming: create, withdraw, cancel ✅ Complete
2 Protocol 26 TTL safety ✅ Complete
3 Delegate withdrawal system ✅ Complete
4 Admin governance and emergency pause ✅ Complete
5 50-test integration suite ✅ Complete
6 Clean GitHub repository with full documentation ✅ Complete
7 Testnet deployment and smoke testing ⏳ Pending
8 Third-party security audit ⏳ Pending
9 Mainnet deployment ⏳ Pending
10 JavaScript/TypeScript SDK ⏳ Future
11 Frontend dApp — stream dashboard for senders and receivers ⏳ Future
12 Cliff vesting variant — lock until cliff date then stream ⏳ Future
13 Milestone-based streaming tied to on-chain attestations ⏳ Future
14 Multi-token stream — multiple SAC tokens in one stream ⏳ Future
15 DAO governance integration — admin controlled by on-chain vote ⏳ Future

15. Alignment with Drips Wave Criteria

Programmable recurring funding — SafeStream enables exactly this. Any organisation sets up a stream once and it runs for months or years, paying contributors or funding public goods without manual intervention at any point.

Protocol 26 TTL mastery — This is the most technically demanding Soroban-specific requirement. SafeStream applies extend_ttl on every persistent storage read and write, with well-chosen constants (30-day bump, 10-day threshold), self-cleaning storage on completion and cancellation, and comprehensive test coverage of storage hygiene invariants. This is the canonical production-safe approach to long-duration state management on Soroban.

Production quality — No raw panics in user-facing logic. Ten typed error codes. Emergency pause mechanism. Transferable admin governance. Fifty integration tests across nine groups. Full event emission for every state transition. Clean public repository. Ready for security audit and testnet deployment today.

Public good — An open-source, MIT-licensed token streaming primitive on Stellar is a genuine ecosystem public good. Any developer building payroll systems, vesting contracts, or recurring funding protocols on Stellar can use or fork SafeStream without restriction.

High complexity tier — The combination of linear vesting math, Protocol 26 TTL management, delegate withdrawal architecture, admin governance with pause circuit breaker, a 50-test suite, and full event coverage places this firmly in the High Complexity category. Every design decision is intentional, documented, and backed by tests.