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: 13 additions & 0 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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);
}
}

// Reject backdated start times so a directly-initialized stream cannot
// already be "running" at creation (the recipient could immediately
// drain a lump sum). Mirrors `create_stream`'s backdated-start guard;
Expand Down
35 changes: 33 additions & 2 deletions contracts/stream/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,35 @@ 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,
&2_592_000_u64,
);
}

/// 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`.
Expand Down Expand Up @@ -1075,15 +1104,17 @@ fn extend_duration_rejects_on_arithmetic_overflow() {
let stream_id = env.register_contract(None, DripStream);
let client = DripStreamClient::new(&env, &stream_id);

// Use an extremely large rate so (rate × 2) overflows i128
// A 1-second stream at the maximum rate: the initial obligation
// (rate × 1) does not overflow, so `initialize` accepts it, but
// extending by 2 s makes `extra_time_seconds × rate` overflow i128.
let huge_rate: i128 = i128::MAX;
client.initialize(
&sender,
&recipient,
&token_addr,
&huge_rate,
&now,
&(now + 10),
&(now + 1),
&false,
&2_592_000_u64,
);
Expand Down
Loading