Skip to content

fix(contracts): preserve accrued vest time on stream top-up (closes #786)#961

Open
samjay8 wants to merge 1 commit into
LabsCrypt:mainfrom
samjay8:fix/786-topup-resets-last-update-time
Open

fix(contracts): preserve accrued vest time on stream top-up (closes #786)#961
samjay8 wants to merge 1 commit into
LabsCrypt:mainfrom
samjay8:fix/786-topup-resets-last-update-time

Conversation

@samjay8

@samjay8 samjay8 commented Jun 30, 2026

Copy link
Copy Markdown

(closes #786)

Summary

top_up_stream was unconditionally resetting stream.last_update_time to the
current ledger timestamp on every call. Since calculate_claimable uses
last_update_time as the accrual anchor (elapsed = effective_now - last_update_time), this silently discarded any vested-but-unwithdrawn
balance the moment a top-up happened.

Concrete impact: for a 1000-token / 1000s stream (rate 1/s), if the recipient
had 900 unwithdrawn tokens vested at t=900 and the sender called
top_up_stream(+100), last_update_time jumped to 900, zeroing
get_claimable_amount. A subsequent cancel_stream at t=901 would then pay
the recipient only 1 token and refund 1099 to the sender — letting the
sender reclaim ~899 tokens the recipient had already earned.

Fix

  • Removed the stream.last_update_time = env.ledger().timestamp() assignment
    in top_up_stream (contracts/stream_contract/src/lib.rs) so prior accrued
    time is preserved across top-ups, including while a stream is paused
    (last_update_time is never pushed past paused_at).

Tests added

  • test_top_up_preserves_already_accrued_claimable — after
    create_stream(1000, 1000s), advancing 900s and topping up by 100 keeps
    get_claimable_amount at 900 instead of dropping to 0.
  • test_top_up_then_cancel_pays_pre_topup_accrued — topping up and
    immediately cancelling pays the recipient exactly the pre-top-up accrued
    amount.
  • test_top_up_while_paused_does_not_advance_last_update_time — topping up a
    paused stream does not push last_update_time past paused_at, and
    claimable amount still reflects only the time accrued before the pause.

Scope

  • Out of scope per the issue: protocol fee logic and frontend/backend top-up
    wiring were left untouched.
  • Files touched: contracts/stream_contract/src/lib.rs,
    contracts/stream_contract/src/test.rs.

top_up_stream reset stream.last_update_time to the current ledger
timestamp on every call. Since calculate_claimable uses
last_update_time as the accrual anchor, this silently zeroed out any
vested-but-unwithdrawn balance: a sender could top up a stream right
before cancelling it to reclaim tokens the recipient had already
earned.

Drop the reassignment so prior accrued time carries through top-ups,
including while the stream is paused (last_update_time must never be
pushed past paused_at). Add regression coverage for the claimable
amount surviving a top-up, a cancel-immediately-after-top-up payout,
and the paused-stream case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Contracts] top_up_stream resets last_update_time, discarding recipient's already-vested tokens (sender can reclaim them via cancel)

1 participant