# missing isEpochClaimed validation

RewardsManager.moveStakedLiquidity calls _claimRewards without checking isEpochClaimed, letting a staker re-claim rewards for an already-claimed epoch.

- Payout: Contest reward
- Severity: high
- Program: Ajna Protocol
- Credited: Jorgect
- Disclosed: 2023-06-29
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2023-05-ajna-findings/issues/132
- Library page: https://bugbountylibrary.com/r/c4%3A2023-05-ajna%3AH-10
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** RewardsManager.sol staking/rewards contract
**Root cause:** _claimRewards lacks the isEpochClaimed[tokenId][epoch] guard that claimRewards enforces; moveStakedLiquidity invokes _claimRewards directly, so the already-claimed epoch is not rejected.
**Steps:**
1. Stake a position via RewardsManager to obtain tokenId.
2. Call `claimRewards(tokenId, epochToClaim)` to receive rewards for the epoch.
3. Call `moveStakedLiquidity(tokenId, fromBuckets, toBuckets, expiry)`; it internally calls `_claimRewards(stakeInfo, tokenId, curBurnEpoch, false, ajnaPool)` with no isEpochClaimed check.
4. Observe rewards for the same epoch are calculated/transferred again instead of reverting AlreadyClaimed.
**How found:** Code review comparing claimRewards (which checks isEpochClaimed) against moveStakedLiquidity's direct call to _claimRewards, noticing the missing guard in the shared internal function.
**Exploitation:** Staker claims rewards for an epoch, then repeatedly calls moveStakedLiquidity (moving liquidity between buckets) to trigger _claimRewards for the same epoch, extracting rewards multiple times.
**Insight:** When an internal helper is called from multiple external entrypoints, check whether each caller replicates the guards (like isEpochClaimed) that the primary entrypoint enforces; the secondary path is where the check is dropped.
