Skip to content

feat: Streaming scholarship stipends — continuous per-ledger fund release #1057

Description

@Anuoluwapo25

Summary

Build a streaming stipend contract — a Soroban contract that releases scholarship funds continuously, ledger by ledger, instead of in lump-sum tranches. A scholar accrues their stipend in real time and withdraws whenever they want; a sponsor can pause, cancel, or top up, and unearned funds return to the treasury. New feature; contracts/milestone_escrow releases discrete tranches only, with no time-based accrual.

Background

Lump-sum tranches fit the funder's calendar, not the learner's life. A scholar who receives three months of funding at once has to budget it themselves under pressure; a sponsor who wants to stop funding an inactive scholar has already sent the money and cannot get it back.

Streaming fixes both sides at once. The scholar sees a balance that ticks upward while they study — a genuinely motivating thing to watch — and the sponsor's remaining capital stays reclaimable. Stellar's low fees are what make frequent small withdrawals actually viable; on a chain with meaningful gas this design does not work.

What to build

1. Soroban contract contracts/stipend_stream/

Storage per stream: sponsor, recipient, token, total_amount, start_ledger, end_ledger, withdrawn, status.

Functions:

  • create_stream(sponsor, recipient, token, amount, start_ledger, end_ledger) — transfers the full amount into the contract up front, so the stream is always fully collateralized. A stream that can run dry is not a stream.
  • withdrawable(stream_id) -> i128 — linear accrual:
    elapsed = min(current_ledger, end_ledger) − start_ledger
    earned = total_amount × elapsed / (end_ledger − start_ledger)
    withdrawable = earned − withdrawn
    Use checked arithmetic throughout; multiply before dividing to avoid truncation losses on small withdrawals.
  • withdraw(stream_id) — recipient-authorized, transfers withdrawable, increments withdrawn.
  • pause_stream / resume_stream — sponsor or DAO authorized. Accrual must stop while paused and the paused duration must extend end_ledger, otherwise a pause silently confiscates the scholar's time.
  • cancel_stream — pays the recipient everything earned to date, refunds the unearned remainder to the sponsor. This split is the contract's most important invariant.
  • top_up_stream — sponsor adds funds and extends end_ledger.

2. Rust tests

  • Accrual at 0%, 50%, 100%, and past end_ledger (must clamp, never exceed total_amount).
  • withdrawn + refunded == total_amount after cancel, for a range of cancellation points — assert this as an explicit invariant.
  • Pause freezes accrual and shifts the end ledger by exactly the paused duration.
  • Unauthorized withdraw, cancel, and pause all panic.
  • Repeated withdraw calls in the same ledger cannot double-pay.

3. Backend

  • Index StreamCreated / Withdrawn / Cancelled events through the existing event indexer (see TODO.md — the poller and events table already exist).
  • Migration server/src/db/migrations/029_stipend_streams.sql plus .undo.sql mirroring stream state for fast queries.
  • GET /api/streams?recipient= and GET /api/streams/:id.

4. Frontend

  • src/hooks/useStipendStream.ts and a stream card on the scholar dashboard: a live-ticking withdrawable balance (interpolate between ledger closes client-side), total streamed, time remaining, and a Withdraw button.
  • Sponsor view: create, pause, top up, cancel, with a clear preview of the earned/refunded split before cancelling is confirmed.

Acceptance criteria

  • Scholar accrues funds continuously and can withdraw partial amounts at any time
  • Accrual clamps at total_amount and never over-pays
  • Cancel splits funds correctly, proven by the withdrawn + refunded == total_amount invariant test
  • Pause halts accrual and extends the end ledger by the paused duration
  • All state-changing functions enforce authorization, with tests for each
  • Dashboard shows a live-updating withdrawable balance
  • Events are indexed and queryable over the API

Notes for contributors

  • Follow the existing contract layout and test conventions in contracts/milestone_escrow/.
  • Ledger-based timing is preferred over timestamps for accrual math; if you use timestamps, justify it in the PR.
  • Rounding is where streaming contracts leak value. State your rounding direction and prove with a test that the contract can never pay out more than total_amount.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbackendAPI/database/infrastructureenhancementNew feature or requestfeatureNew feature implementationfrontendReact/TypeScript UI worksmart-contractSoroban/Rust contract work

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions