Skip to content

Commit bffc717

Browse files
authored
Merge pull request #961 from samjay8/fix/786-topup-resets-last-update-time
fix(contracts): preserve accrued vest time on stream top-up (closes #786)
2 parents d1d25e4 + 3839e8c commit bffc717

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

contracts/stream_contract/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,10 @@ impl StreamContract {
288288
// Collect protocol fee and get net amount
289289
let net_amount = Self::collect_fee(&env, &stream.token_address, amount, stream_id);
290290

291-
// Update stream state
291+
// Update stream state. `last_update_time` is intentionally left untouched:
292+
// it is the accrual anchor for `calculate_claimable`, and advancing it to
293+
// `now` would discard any already-vested, unwithdrawn tokens.
292294
stream.deposited_amount += net_amount;
293-
stream.last_update_time = env.ledger().timestamp();
294295

295296
save_stream(&env, stream_id, &stream);
296297

contracts/stream_contract/src/test.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,53 @@ fn test_top_up_emits_event() {
497497
assert_eq!(payload.new_deposited_amount, 15_000);
498498
}
499499

500+
#[test]
501+
fn test_top_up_preserves_already_accrued_claimable() {
502+
let env = Env::default();
503+
env.mock_all_auths();
504+
let (token, _) = create_token(&env);
505+
let sender = Address::generate(&env);
506+
let recipient = Address::generate(&env);
507+
mint(&env, &token, &sender, 2_000);
508+
509+
let client = create_contract(&env);
510+
let id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000);
511+
512+
// Recipient vests 900 tokens (rate 1/sec) before the top-up.
513+
env.ledger().with_mut(|l| l.timestamp += 900);
514+
assert_eq!(client.get_claimable_amount(&id), Some(900));
515+
516+
client.top_up_stream(&sender, &id, &100);
517+
518+
// Already-accrued, unwithdrawn time must survive the top-up.
519+
assert_eq!(client.get_claimable_amount(&id), Some(900));
520+
}
521+
522+
#[test]
523+
fn test_top_up_then_cancel_pays_pre_topup_accrued() {
524+
let env = Env::default();
525+
env.mock_all_auths();
526+
let (token, _) = create_token(&env);
527+
let sender = Address::generate(&env);
528+
let recipient = Address::generate(&env);
529+
mint(&env, &token, &sender, 2_000);
530+
531+
let client = create_contract(&env);
532+
let id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000);
533+
534+
env.ledger().with_mut(|l| l.timestamp += 900);
535+
client.top_up_stream(&sender, &id, &100);
536+
537+
let token_client = token::Client::new(&env, &token);
538+
let recipient_balance_before = token_client.balance(&recipient);
539+
540+
// Cancel immediately after the top-up — no further time should accrue.
541+
client.cancel_stream(&sender, &id);
542+
543+
let recipient_balance_after = token_client.balance(&recipient);
544+
assert_eq!(recipient_balance_after - recipient_balance_before, 900);
545+
}
546+
500547
// ─── withdraw ────────────────────────────────────────────────────────────────
501548

502549
#[test]
@@ -1758,6 +1805,34 @@ fn test_top_up_while_paused_increases_deposited() {
17581805
assert!(new_deposited > old_deposited);
17591806
}
17601807

1808+
#[test]
1809+
fn test_top_up_while_paused_does_not_advance_last_update_time() {
1810+
let env = Env::default();
1811+
env.mock_all_auths();
1812+
let (token, _) = create_token(&env);
1813+
let sender = Address::generate(&env);
1814+
let recipient = Address::generate(&env);
1815+
mint(&env, &token, &sender, 2_000);
1816+
1817+
let client = create_contract(&env);
1818+
let id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000);
1819+
1820+
// Accrue 300s, then pause.
1821+
env.ledger().with_mut(|l| l.timestamp += 300);
1822+
client.pause_stream(&sender, &id);
1823+
1824+
// More ledger time passes while paused; top up during this window.
1825+
env.ledger().with_mut(|l| l.timestamp += 200);
1826+
client.top_up_stream(&sender, &id, &100);
1827+
1828+
let stream = client.get_stream(&id).unwrap();
1829+
assert!(stream.last_update_time <= stream.paused_at.unwrap());
1830+
1831+
// Claimable should still reflect the 300s accrued before the pause, not be
1832+
// wiped out by the top-up pushing last_update_time past paused_at.
1833+
assert_eq!(client.get_claimable_amount(&id), Some(300));
1834+
}
1835+
17611836
#[test]
17621837
fn test_withdraw_after_long_stream_runtime_is_bounded() {
17631838
let env = Env::default();

0 commit comments

Comments
 (0)