Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn cancel(env: Env, caller: Address) -> Result<(), Error>
fn pause(env: Env, caller: Address) -> Result<(), Error>
fn resume(env: Env, caller: Address) -> Result<(), Error>
fn top_up(env: Env, caller: Address, amount: i128) -> Result<(), Error>
fn clawback(env: Env, caller: Address) -> Result<i128, Error>
fn clawback(env: Env, caller: Address) -> Result<i128, Error> // rejected while paused; resume() first

// Extend end_time by extra_time_seconds, pulling the exact rate-implied deposit from the sender
fn extend_duration(env: Env, caller: Address, extra_time_seconds: u64) -> Result<(), Error>
Expand Down
7 changes: 7 additions & 0 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,10 @@ impl DripStream {
}

/// Sender reclaims unstreamed tokens (only if clawback was enabled).
///
/// A paused stream must be resumed before clawback is allowed; otherwise the
/// sender could freeze accrual and immediately drain the remaining principal
/// while the recipient is effectively blocked from earning any more funds.
pub fn clawback(env: Env, caller: Address) -> Result<i128, Error> {
state::with_guard(&env, |env| Self::_clawback(env, &caller))
}
Expand All @@ -507,6 +511,9 @@ impl DripStream {

let info = state::load(env);
state::assert_not_cancelled(&info)?;
if info.is_paused() {
return Err(Error::NotPaused);
}
if !info.is_clawback_enabled() {
return Err(Error::ClawbackDisabled);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ A malicious sender pausing a stream indefinitely, blocking further accrual for t

A sender with `clawback_enabled = true` can call `clawback()` to reclaim all unstreamed tokens at any time, effectively starving the recipient of future payments. Recipients should verify `clawback_enabled` before accepting a stream.

**Mitigation:** The app should display a prominent warning when `clawback_enabled = true`. Consider requiring recipient acknowledgement before stream activation.
**Mitigation:** The app should display a prominent warning when `clawback_enabled = true`. Additionally, the protocol blocks `clawback()` while a stream is paused; the sender must `resume()` before reclaiming any unstreamed balance. Consider requiring recipient acknowledgement before stream activation.

---

Expand Down
17 changes: 5 additions & 12 deletions tests/stream_clawback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,25 +168,18 @@ fn clawback_on_cancelled_stream_is_rejected() {
// ── Paused stream clawback ───────────────────────────────────────────────────

#[test]
fn clawback_while_paused_uses_paused_timestamp() {
fn clawback_while_paused_is_rejected() {
let env = base_env();
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

let (client, token_addr) =
deploy_stream_with_clawback(&env, &sender, &recipient, 1_000, 3_600, true);
let tok = token::Client::new(&env, &token_addr);
let (client, _) = deploy_stream_with_clawback(&env, &sender, &recipient, 1_000, 3_600, true);

advance(&env, 300); // 300_000 streamed at pause point
advance(&env, 300);
client.pause(&sender);
advance(&env, 500); // paused — no additional accrual

// Owed = 300_000; unstreamed = 3_300_000
let sender_before = tok.balance(&sender);
let reclaimed = client.clawback(&sender);

assert_eq!(reclaimed, 3_300_000);
assert_eq!(tok.balance(&sender) - sender_before, 3_300_000);
let result = client.try_clawback(&sender);
assert_eq!(result, Err(Ok(Error::NotPaused)));
}

// ── Clawback enabled view function ───────────────────────────────────────────
Expand Down