diff --git a/contracts/stream/src/lib.rs b/contracts/stream/src/lib.rs index 81d5d290..9c4fb167 100644 --- a/contracts/stream/src/lib.rs +++ b/contracts/stream/src/lib.rs @@ -336,6 +336,15 @@ impl DripStream { .checked_sub(info.paused_at) .ok_or(Error::ArithmeticOverflow)?; + // A stream that stays paused beyond the protocol's safe grace window is + // at risk of instance-storage archival; reject the resume before the host + // turns this into the opaque "entry archived" error path. The same + // threshold is used by `force_cancel()` to keep the contract-level safety + // policy consistent across both recovery flows. + if paused_duration > ttl::MAX_PAUSE_SECS { + return Err(Error::PauseThresholdNotMet); + } + // Shift start_time forward by paused duration so paused time doesn't // count; end_time is shifted by the same amount on resume so the // contracted duration is preserved in wall-clock terms. @@ -617,8 +626,6 @@ impl DripStream { } fn _force_cancel(env: &Env) -> Result<(), Error> { - const PAUSE_THRESHOLD_SECS: u64 = 2_592_000; // 30 days - ttl::bump(env); let info = state::load(env); @@ -629,7 +636,7 @@ impl DripStream { let now = env.ledger().timestamp(); let paused_secs = now.saturating_sub(info.paused_at); - if paused_secs < PAUSE_THRESHOLD_SECS { + if paused_secs < ttl::MAX_PAUSE_SECS { return Err(Error::PauseThresholdNotMet); } diff --git a/contracts/stream/src/ttl.rs b/contracts/stream/src/ttl.rs index 5578a625..50c2c197 100644 --- a/contracts/stream/src/ttl.rs +++ b/contracts/stream/src/ttl.rs @@ -2,6 +2,15 @@ use soroban_sdk::Env; use drip_common::{TTL_EXTEND_TO, TTL_THRESHOLD}; +/// Maximum safe duration a stream may remain paused before the instance +/// storage TTL window is no longer sufficient to resume it safely. +/// +/// A single `extend_ttl` bump only renews the instance record to +/// `TTL_EXTEND_TO` (200_000 ledgers). Any pause that exceeds the window can +/// leave the stream archived before a normal `resume()` or `force_cancel()` +/// call can run. +pub const MAX_PAUSE_SECS: u64 = 2_592_000; // 30 days + pub fn bump(env: &Env) { env.storage() .instance() diff --git a/tests/stream_pause_resume.rs b/tests/stream_pause_resume.rs index 09ca5d27..58f9313c 100644 --- a/tests/stream_pause_resume.rs +++ b/tests/stream_pause_resume.rs @@ -142,6 +142,20 @@ fn resume_on_running_stream_is_rejected() { assert_eq!(result, Err(Ok(Error::NotPaused))); } +#[test] +fn resume_after_safe_pause_window_is_rejected() { + let env = base_env(); + let sender = Address::generate(&env); + let recip = Address::generate(&env); + let (client, _) = deploy_stream(&env, &sender, &recip, 100, 3_600); + + client.pause(&sender); + advance(&env, 2_592_001); + + let result = client.try_resume(&sender); + assert_eq!(result, Err(Ok(Error::PauseThresholdNotMet))); +} + // ── Recipient can withdraw while paused ────────────────────────────────────── #[test]