The claimRewards function in the FeeCollector contract is designed to allow users to claim their accumulated rewards based on their voting power. The rewards are calculated using the formula:
pendingReward = share - userRewards[user]
where:
share = (totalDistributed * userVotingPower) / totalVotingPower
However, the function mistakenly resets the user's reward record by setting userRewards[user] equal to totalDistributed rather than updating it to the user's current proportional share. This error means that once a user claims rewards, their recorded reward balance is set too high, so subsequent calculations yield a pending reward of zero. As a result, users who have claimed previously may never be able to claim additional rewards even if their voting power increases, thus causing a denial of service for reward claims.
Reward Calculation Flow:
The reward claim process involves:
Calculating the user's share:
share = (totalDistributed * veRAACToken.getVotingPower(user)) / veRAACToken.getTotalVotingPower()
Determining the pending reward as:
pendingReward = share - userRewards[user]
Faulty Reset Logic in claimRewards:
In the claimRewards function, after calculating pendingReward, the function resets userRewards[user] to totalDistributed:
This is incorrect. Instead, userRewards[user] should be updated to the user's share value, not the total distributed amount. Resetting it to totalDistributed overstates the user's cumulative claimed rewards. Therefore, subsequent reward calculations will yield:
pendingReward = share - totalDistributed
which is zero or negative, effectively blocking further claims.
Impact of the Bug:
As users claim rewards, their userRewards value becomes too high, and future claims return 0 even when their voting power increases. This results in a denial of service (DoS) for reward claims, undermining both reward distribution and user trust in the protocol.
claimRewards Function:
_calculatePendingRewards Function:
The error in claimRewards prevents future rewards from accumulating, as userRewards is set to an excessive value.
Initial Reward Claim:
A user (e.g., Alice) locks tokens and accumulates a certain voting power.
At time T1, totalDistributed is, say, 3,859,228e18.
The user's share is calculated as:
share = (3,859,228e18 * AliceVotingPower) / TotalVotingPower
Suppose Alice’s pending reward is computed as 964,807e18.
When she claims, the function resets her reward record to totalDistributed (3,859,228e18).
Subsequent Reward Claims:
At time T2, if Alice's voting power increases, her new share is recalculated. However, since userRewards[ALICE] is already set to a very high value (totalDistributed), the pending reward becomes:
pendingReward = newShare - totalDistributed; approx 0
As a result, Alice cannot claim any further rewards, causing a denial of service.
Below is the test suite demonstrating the issue:
Initial Claim:
ALICE is able to claim an initial reward based on her voting power.
After Voting Power Increase:
When ALICE increases her voting power, the subsequent call to claimRewards reverts because userRewards[ALICE] is incorrectly set to totalDistributed from the first claim, which is far higher than her proportional share.
Resulting DoS:
ALICE (and similarly other users) are effectively locked out of future reward claims.
Denial of Service for Reward Claims:
Users who have previously claimed rewards cannot claim further rewards, even if their voting power increases, as their recorded rewards are set too high. This leads to a persistent denial of service for reward claims.
Economic Distortion:
The miscalculation undermines the fairness of reward distribution. Users receive less than their due share, which can discourage participation and damage trust in the protocol.
Governance Implications:
Reduced rewards affect the overall incentive structure and may distort governance, as rewards are a critical component of stakeholder influence.
Manual Review
Foundry
To fix this vulnerability, update the claimRewards function so that it correctly updates userRewards[user] to reflect the user’s proportional share of totalDistributed rather than resetting it to totalDistributed. The correct approach is to set userRewards[user] equal to:
userRewards[user] = pendingReward;
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.