diff --git a/README.md b/README.md index 041f135b..4bda3939 100644 --- a/README.md +++ b/README.md @@ -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 +fn clawback(env: Env, caller: Address) -> Result // 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> diff --git a/contracts/stream/src/lib.rs b/contracts/stream/src/lib.rs index 4264d8c4..06a29df9 100644 --- a/contracts/stream/src/lib.rs +++ b/contracts/stream/src/lib.rs @@ -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 { state::with_guard(&env, |env| Self::_clawback(env, &caller)) } @@ -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); } diff --git a/docs/security.md b/docs/security.md index 007e7488..3a5c860f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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. --- diff --git a/tests/stream_clawback.rs b/tests/stream_clawback.rs index fa73fc15..47cbf797 100644 --- a/tests/stream_clawback.rs +++ b/tests/stream_clawback.rs @@ -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 ───────────────────────────────────────────