# Staker can perform rewards withdrawal every overdueBlocks less 1 and have zero frozenCoinAge

Staker calls withdrawRewards every overdueBlocks-1 to keep repayDiff below overdue threshold, zeroing frozenCoinAge bad-debt penalty and inflating UNION rewards.

- Payout: Contest reward
- Severity: high
- Program: Union
- Credited: hyh
- Disclosed: 2023-02-07
- Write-up quality: 8/10
- Original: https://github.com/sherlock-audit/2023-02-union-judging/issues/25
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-3
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic vuln:reward-distribution

**Target:** UserManager rewards accounting (Solidity/UToken)
**Root cause:** _getEffectiveAmounts computes repayDiff as block.number - max(lastRepay, coinAge.lastWithdrawRewards), where lastWithdrawRewards is staker-controlled; keeping it fresh keeps repayDiff <= overdueBlocks so frozenCoinAge is never accumulated.
**Steps:**
1. Staker vouches for a borrower (or self-lends) and accrues locked coinage.
2. Borrower stops repaying; bad debt accrues but no overdue repayment triggers onRepayBorrow, so frozenCoinAge[staker] stays 0.
3. Staker calls `withdrawRewards()` at least once every `uToken.overdueBlocks() - 1` blocks, updating getLastWithdrawRewards.
4. In `_getEffectiveAmounts`, `repayDiff = block.number - _max(lastRepay, lastWithdrawRewards)` stays <= overdueBlocks, so the `overdueBlocks < repayDiff` branch never adds to memberTotalFrozen/frozenCoinAge.
5. Effective amounts remain undiluted; staker collects full UNION rewards despite bad debt.
**How found:** Code reading of _getEffectiveAmounts and _getCoinAge revealed lastWithdrawRewards is staker-controlled and feeds the overdue comparison, while frozenCoinAge is only written in onRepayBorrow on overdue repayment; hypothesis that frequent withdrawals suppress the penalty was confirmed by tracing the condition.
**Exploitation:** Any staker times withdrawRewards() calls to just under overdueBlocks, or self-lends with no interest and withdraws on schedule, harvesting full reward multiplier with zero frozenCoinAge penalty and diluting honest stakers via excess UNION emission.
**Insight:** When a penalty accumulator is gated on a time delta that includes a user-writable timestamp (lastWithdrawRewards), check whether refreshing that timestamp can keep the delta under the threshold and skip the penalty entirely; also check whether the penalty is only written on a rare event path (overdue repay) rather than continuously.
