# ts.tokens sometimes calculated incorrectly

In Locke.sol, staking after a full withdraw leaves ts.lastUpdate stale because updateStream skips when ts.tokens==0, letting a later withdraw compute inflated ts.tokens and over-withdraw.

- Payout: Audit contest
- Severity: high
- Program: Streaming Protocol
- Found by: gpersoon
- Disclosed: 2022-02-11
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2021-11-streaming-findings/issues/123
- Library page: https://bugbountylibrary.com/r/c4%3A2021-11-streaming%3AH-08
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:solidity technique:source-review vuln:accounting-error

**Target:** Locke.sol staking/streaming contract (stake/withdraw/updateStreamInternal)
**Root cause:** updateStreamInternal only updates ts.lastUpdate inside the `acctTimeDelta > 0 && ts.tokens > 0` branch, so when ts.tokens==0 (post full withdraw) a subsequent stake does not refresh lastUpdate, and the next withdraw computes streamed tokens over an inflated time delta.
**Steps:**
1. Stake tokens via `stake(amount)`; ts.tokens>0, lastUpdate set.
2. Withdraw all tokens via `withdraw(amount)`; ts.tokens becomes 0.
3. Wait some time, then `stake(amount)` again: updateStream runs but `ts.tokens > 0` is false at entry, so ts.lastUpdate stays at the first withdraw time.
4. Call `withdraw(amount)`: updateStreamInternal computes `acctTimeDelta = block.timestamp - ts.lastUpdate` using the stale (old) lastUpdate, subtracting too little from ts.tokens.
5. Withdraw more than entitled; repeat to drain excess streamed tokens.
**How found:** Code-reading of updateStreamInternal: noticed ts.lastUpdate is only assigned inside the `ts.tokens > 0` guard, hypothesized that a zero-balance state skips the timestamp refresh, then traced stake/withdraw flows to confirm the stale-delta miscalculation.
**Exploitation:** Attacker with a normal staking position performs stake→full withdraw→wait→re-stake→withdraw, exploiting the stale lastUpdate to have ts.tokens under-decremented and withdraw more tokens than accrued.
**Insight:** When a time-weighted accounting function guards both the accrual math and the timestamp update under the same `balance > 0` condition, a zero-balance transition (full exit) freezes the timestamp; check every early-return/guard for whether lastUpdate must still advance independently of the balance.
