# RewardsManager doesn't delete old bucket snapshot info on unstaking

RewardsManager.unstake() deletes StakeInfo but not its nested snapshot mapping, so restaked tokenIds reuse stale bucket snapshots to over-claim interest rewards.

- Payout: Contest reward
- Severity: high
- Program: Ajna
- Credited: hyh
- Disclosed: 2022-12-21
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-01-ajna-judging/issues/183
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-01-ajna%3AH-1
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:reward-distribution

**Target:** RewardsManager.sol staking/rewards contract
**Root cause:** Solidity delete on a struct does not recurse into nested mappings, so stakes[tokenId_].snapshot persists after unstake; restaking only overwrites buckets in the current position index list, leaving stale BucketState entries for buckets re-added later.
**Steps:**
1. Stake LP NFT tokenId with bucket set A; snapshot[A] written with lpsAtStakeTime and rateAtStakeTime.
2. Call unstake(tokenId): `delete stakes[tokenId_]` clears owner/epoch but snapshot[A] persists.
3. Restake tokenId with a different bucket set B (possibly intersecting A); only snapshot[B] overwritten.
4. Use PositionManager.moveLiquidity() to re-add buckets from A into positionIndexes.
5. Call _claimRewards: _calculateNextEpochRewards reads stale snapshot[A] rateAtStakeTime/lpsAtStakeTime, computing interest from the first stake time.
6. Collect inflated rewards at expense of other stakers.
**How found:** Code review of unstake()'s `delete stakes[tokenId_]` combined with knowledge that Solidity delete does not traverse nested mappings; traced snapshot writes in stake() and reads in _calculateNextEpochRewards to confirm stale values are reused.
**Exploitation:** Attacker controls an LP NFT, stakes, unstakes, restakes over a different bucket composition, then moves liquidity to re-add previously snapshotted buckets so stale rateAtStakeTime (older, lower rate) is reused, inflating interestEarned via _calculateExchangeRateInterestEarned and draining extra rewards from the pool.
**Insight:** When auditing staking/reward contracts, check whether `delete` on a struct containing a mapping actually clears per-bucket accounting; stale snapshot entries for buckets re-added via position moves let a restaker backdate their stake time and over-claim.
