# Staker can manipulate lockedCoinAge and earn rewards in excess of the allowed maximum

Staker's vouch.lastUpdated not refreshed when trust is maxed during borrow, letting repeated min-borrows inflate lockedCoinAge and drain all 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/26
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-2
- 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.updateLocked / Comptroller rewards
**Root cause:** updateLocked() adds (block.number - max(lastWithdrawRewards, vouch.lastUpdated)) * vouch.locked to lockedCoinAge for every vouch, but when lock==true and vouch.trust==vouch.locked, lockAmount==0 causes continue before vouch.lastUpdated is refreshed, so the same locked amount is re-counted each call.
**Steps:**
1. Staker Bob has a vouch with trust maxed: vouch.trust == vouch.locked == 10k DAI.
2. Bob creates a second borrower account with minimal trust.
3. Bob waits some blocks so (block.number - vouch.lastUpdated) is positive.
4. Bob repeatedly calls UToken.borrow(1 DAI) via the second borrower (amount > minBorrow), each triggering updateLocked(..., true).
5. Each call re-adds (block.number - vouch.lastUpdated) * 10k DAI to stakers[Bob].lockedCoinAge without updating vouch.lastUpdated.
6. Bob's effectiveLocked grows unbounded, inflating lendingRatio in _getRewardsMultiplier.
7. Bob claims rewards via Comptroller, receiving arbitrary large UNION.
**How found:** Code reading of updateLocked(): noticed lockedCoinAge accumulator uses vouch.lastUpdated but the renewal only happens inside the lock/unlock branches after the lockAmount==0 continue, so maxed vouches never refresh lastUpdated.
**Exploitation:** Attacker needs a maxed vouch and a second borrower account. By looping minimal borrows (each > minBorrow) through UToken.borrow, updateLocked runs with lock=true; the maxed vouch hits lockAmount==0 and continues before lastUpdated renewal, so lockedCoinAge accumulates the full locked amount per call. The inflated effectiveLocked raises the reward multiplier, and the attacker claims all UNION from the reward contract.
**Escalation:** Waiting more blocks before each borrow increases the per-call lockedCoinAge delta, amplifying the multiplier and total drain.
**Insight:** When an accumulator uses a per-record timestamp, check every early-continue path between the accumulation and the timestamp write; a maxed/saturated record that skips the write lets the same value be re-counted each call.
