Skip to content

[Smart Contracts] Implement Batch Withdrawal Functionality ('batch_withdraw') for Multi-Stream Recipients #1438

Description

@blurbeast

Overview & Background

In standard payment streaming architectures, recipients frequently receive multiple concurrent income streams—such as contractors working for multiple DAOs, employees receiving base compensation and vested incentive bonuses separately, or grant recipients receiving funding from multiple tranches. Currently, FlowFi's stream_contract only provides a single-stream withdrawal entrypoint:

pub fn withdraw(env: Env, recipient: Address, stream_id: u64) -> Result<i128, StreamError>

Calling withdraw separately for $N$ streams requires $N$ separate ledger transactions, incurring repeated base fees, multiple user wallet confirmations, sequence number contention, and unnecessary ledger overhead.


Detailed Problem Statement

  1. High Transaction & Overhead Costs:
    • Recipients receiving 5–20 streams must broadcast individual transactions for each stream to collect their vested earnings.
    • Each transaction consumes network base fees and ledger write footprints.
  2. Poor User Experience:
    • The recipient's wallet (e.g. Freighter) must prompt for multiple signatures sequentially, causing friction and drop-off.
  3. Risk of Partial Failure:
    • If an intermediate transaction fails due to network spikes or sequence number mismatches, the recipient is left in a partially withdrawn state.

Technical Specification & Architecture

1. Batch Withdrawal Entrypoint

Implement batch_withdraw in contracts/stream_contract/src/lib.rs:

pub fn batch_withdraw(
    env: Env,
    recipient: Address,
    stream_ids: Vec<u64>,
) -> Result<Vec<(u64, i128)>, StreamError>

2. Authorization & Invariant Checks

  • Strictly enforce recipient.require_auth().
  • Validate that stream_ids.len() <= 30 to prevent exceeding Soroban CPU/memory transaction resource limits.
  • For each stream_id:
    • Load stream from persistent storage via storage::load_stream(&env, stream_id).
    • Verify stream.recipient == recipient.
    • Verify stream.is_active is true.
    • Calculate claimable amount factoring in cliff_time, current ledger timestamp, and paused state.
    • If claimable amount > 0:
      • Update stream.withdrawn_amount and stream.last_update_time.
      • If fully claimed, mark stream.is_active = false and stream.status = StreamStatus::Completed.
      • Save updated stream state and extend TTL.
      • Transfer tokens from contract to recipient via Soroban Token client.
      • Emit TokensWithdrawn event.
      • Record (stream_id, amount_withdrawn) into the return result vector.

Target Files

  • contracts/stream_contract/src/lib.rs
  • contracts/stream_contract/src/events.rs
  • contracts/stream_contract/src/test.rs

Acceptance Criteria

  • batch_withdraw successfully withdraws claimable balances from multiple streams in a single atomic transaction.
  • Enforces recipient authorization; unauthorized callers revert with StreamError::Unauthorized.
  • Streams with zero claimable balance are skipped without reverting the entire batch.
  • Emits individual TokensWithdrawn events for every processed stream.
  • Comprehensive unit tests cover mixed-token streams, partially claimable streams, and full completion transitions.

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions