withdraw skips the checkpoint when the balance falls to zero
- input-1/StreamVault.sol:88-97
- input-1/StreamVault.sol:61-64
- Impact
A staker who withdraws everything forfeits the rewards accrued since their last checkpoint. The forfeited amount stays in the vault and is paid out to the remaining stakers.
- Observed
withdrawcalls_checkpointonly whenremaining != 0(lines 91-93).- A full withdrawal sets the balance to 0 at line 94 without settling
earnedfor the period since the last checkpoint. claimreadsearned[msg.sender], which was never updated for that period (lines 61-64).
input-1/StreamVault.sol88–97 function withdraw(uint256 shares) external nonReentrant { if (shares == 0) revert ZeroAmount(); uint256 remaining = balanceOf[msg.sender] - shares; if (remaining != 0) { _checkpoint(msg.sender); } balanceOf[msg.sender] = remaining; totalShares -= shares; token.safeTransfer(msg.sender, shares); }- Counterargument
A full exit is meant to go through
exit(), which claims first.Resolved
withdrawis external and accepts the full balance. Nothing at lines 88-97 sends a full withdrawal toexit().- Evidence gap
- No test was run. The forfeited amount follows from lines 61-64 and 91-94.
- Fix
Call
_checkpoint(msg.sender)before the balance changes, with no condition onremaining.- Next
Write the test: stake, accrue, withdraw the full balance, then assert
earnedequals the accrued amount.