Target
contracts/core/collectors/FeeCollector.sol
Vulnerability Details
The feeCollector contract is responsible for managing reward periods and user claims. The claimRewards function within the contract allows a user to claim accumulated rewards, after the user’s pending rewards are calculated, the state variable mapping that stores users claimed rewards (userRewards[user]).
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;
}
FeeCollector.claimRewards
However, when a user claims their rewards the total user’s rewards is updated incorrectly to the total distributed in that round rather than the amount claimed, this leads to a situation where whenever the user comes back to for another reward claim, they are considered ineligible due to accounting logic that calculate user’s claims thereby blocking user’s from subsequent reward claims
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}
FeeCollector._calculatePendingRewards
Impact
Users would be unable to claim rewards after first claim, as afterwards, pending rewards will return zero
Tools Used
Manual Review
Recommendations
Users already claimed rewards should to accounted for correctly so subsequent claim requests will not incorrectly revert
POC
add the test script below to test/unit/core/collectors/FeeCollector.test.js
describe("POC #5", function () {
it("sets user's total claimed to total distributed", async function () {
await feeCollector.connect(owner).distributeCollectedFees();
await time.increase(WEEK);
const initialBalance = await raacToken.balanceOf(user1.address);
console.log("--- Initial balance -------");
console.log(initialBalance);
await feeCollector.connect(user1).claimRewards(user1.address);
console.log(await raacToken.balanceOf(user1.address));
console.log(await feeCollector.totalDistributed());
console.log(await feeCollector.userRewards(user1.address));
});
});