Summary
The FeeCollector
contract implements lastClaimTime
mapping that maps user addresses to their last claim timestamp. However, this is not updated during claims as done in BaseGauge
which is used to restrict the frequency with which users perform claims thereby allowing claims to be performed at any given time.
Vulnerability Details
In BaseGauge
, rewards claiming frequency is enforced by the MIN_CLAIM_INTERVAL
. This works in conjunction with the lastClaimTime
mapping as shown in getReward()
:
function getReward() external virtual nonReentrant whenNotPaused updateReward(msg.sender) {
>> if (block.timestamp - lastClaimTime[msg.sender] < MIN_CLAIM_INTERVAL) {
revert ClaimTooFrequent();
}
lastClaimTime[msg.sender] = block.timestamp;
---SNIP---
}
This ensures a minimum interval between reward claims.
However, in FeeCollector
contract, the same lastClaimTime
mapping is implemented with a function to update it yet this is not used at all in claimRewards()
.
* @notice User claim tracking
* @dev Maps user addresses to their last claim timestamp
*/
>> mapping(address => uint256) private lastClaimTime;
---SNIP---
function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
userRewards[user] = totalDistributed;
>>
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}
---SNIP---
function _updateLastClaimTime(address user) internal {
>> lastClaimTime[user] = block.timestamp;
}
Impact
If the same mechanism for claim used in BaseGauge
is intended to be used in FeeCollector
contract as well, then this is not achievable. As such, claims will be done at any time without any checked interval.
Tools Used
Manual Review
Recommendations
Enforce claim intervals to utilize this functionality:
+ /// @audit Provide a minimum interval between reward claims e.g,
+ uint256 public constant MIN_CLAIM_INTERVAL = 1 days;
---SNIP---
function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
+ if (block.timestamp - lastClaimTime[user] < MIN_CLAIM_INTERVAL) {
+ revert ClaimTooFrequent();
+ }
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
// Reset user rewards before transfer
userRewards[user] = totalDistributed;
+ // @audit Update lastClaimTime for the user
+ _updateLastClaimTime(user);
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}