From ae6d74e089dcc8ff6836220a8a6901279e2bf8bd Mon Sep 17 00:00:00 2001 From: Goodness-0x Date: Mon, 31 Aug 2026 09:51:08 +0000 Subject: [PATCH] fix: reject bounded streams whose total obligation overflows --- contracts/stream/src/lib.rs | 13 +++++++++++++ contracts/stream/src/tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/contracts/stream/src/lib.rs b/contracts/stream/src/lib.rs index 4264d8c4..c07a403e 100644 --- a/contracts/stream/src/lib.rs +++ b/contracts/stream/src/lib.rs @@ -91,6 +91,19 @@ impl DripStream { panic_with_error!(&env, Error::InvalidTimeRange); } + // A bounded stream must not overflow its total obligation up front. + // Once enough time elapses, `streamed_amount` multiplies `rate_per_second` + // by `elapsed` and would otherwise return `ArithmeticOverflow` in + // settlement paths like `withdraw` / `cancel` / `clawback`, which would + // permanently lock the escrow. Reject the malformed stream before it is + // persisted. + if end_time > 0 { + let duration = (end_time - start_time) as i128; + if rate_per_second.checked_mul(duration).is_none() { + panic_with_error!(&env, Error::ArithmeticOverflow); + } + } + ttl::bump(&env); let mut flags: u32 = 0; diff --git a/contracts/stream/src/tests.rs b/contracts/stream/src/tests.rs index 11539fdd..25edf7ad 100644 --- a/contracts/stream/src/tests.rs +++ b/contracts/stream/src/tests.rs @@ -472,6 +472,34 @@ fn initialize_rejects_end_time_equal_start() { ); } +#[test] +#[should_panic(expected = "Error(Contract, #12)")] +fn initialize_rejects_overflowing_total_obligation() { + let env = Env::default(); + env.mock_all_auths(); + + let sender = Address::generate(&env); + let recipient = Address::generate(&env); + let token_admin = Address::generate(&env); + let token_addr = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + + let stream_id = env.register_contract(None, DripStream); + let client = DripStreamClient::new(&env, &stream_id); + + let start_time: u64 = 1_000_000; + client.initialize( + &sender, + &recipient, + &token_addr, + &i128::MAX, + &start_time, + &(start_time + 2), + &false, + ); +} + /// The guard must NOT reject legitimate open-ended streams (`end_time == 0`). /// This is the regression fence around the boundary check: `0` is a sentinel /// for "no end", not a time that precedes `start_time`.