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
13 changes: 10 additions & 3 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
9 changes: 9 additions & 0 deletions contracts/stream/src/ttl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 14 additions & 0 deletions tests/stream_pause_resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down