# Rollover rewards are permanently lost due to flawed rewardRate calculation

GaugeCL.notifyRewardAmount computes rewardRate from only the new rewardAmount, ignoring clPool.rollover(), stranding rollover rewards permanently.

- Payout: Contest reward
- Severity: medium
- Program: Hybra Finance
- Credited: osok
- Disclosed: 2025-11-20
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-10-hybra-finance#m-05
- Library page: https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-05
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:reward-distribution

**Target:** GaugeCL.sol notifyRewardAmount / clPool.syncReward
**Root cause:** When block.timestamp >= _periodFinish, rewardRate is set to rewardAmount/epochTimeRemaining while rewardReserve is set to rewardAmount + clPool.rollover(), so the reserve exceeds what the rate can emit; the else branch compounds the error using the already-flawed rate.
**Steps:**
1. Observe notifyRewardAmount: `totalRewardAmount = rewardAmount + clPool.rollover()`
2. In the `block.timestamp >= _periodFinish` branch, note `rewardRate = rewardAmount / epochTimeRemaining` excludes rollover
3. `clPool.syncReward({rewardRate, rewardReserve: totalRewardAmount, periodFinish})` sets reserve > rate*time
4. Rollover portion never emitted; next epoch overwrites it, permanently erasing it
5. Fix: `rewardRate = totalRewardAmount / epochTimeRemaining`
**How found:** Code review of the epoch-rollover branch in notifyRewardAmount, spotting the mismatch between rewardReserve (includes rollover) and rewardRate (excludes it).
**Insight:** In staking/reward contracts, always cross-check that rewardReserve equals rewardRate * duration; any reserve/rate mismatch strands or over-emits tokens, especially across epoch boundaries where rollover is folded in.
