Summary
The FeeCollector::claimRewards
lacks restriction on who can call the function, allowing anyone to claim rewards on behalf of someone else.
Vulnerability Details
The FeeCollector::claimRewards
is designed to ensure users can claim their rewards:-
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;
}
However, as we can see there is no restriction on who can call this function on behalf of, hence allowing anyone to claim rewards for someone else who is not even willing to claim.
Impact
This is sub-optimal as users who would not want to claim rewards for a certain time period would be forced claimed.
Not claiming rewards can also be seen as a way to save funds for future usecase or simply to show trust on the protocol.
Proof of Concept
Add the below test case inside the FeeCollector.test.js
file:
describe("Reward Claim Issue", async function() {
it("should allow anyone to claim rewards for anyone", async function () {
await feeCollector.connect(owner).distributeCollectedFees();
await time.increase(WEEK);
const initialBalance = await raacToken.balanceOf(user1.address);
const [userRandom] = await ethers.getSigners();
await feeCollector.connect(userRandom).claimRewards(user1.address);
expect(await raacToken.balanceOf(user1.address)).to.be.gt(initialBalance);
});
})
As we can observe, a random address just claimed rewards for a user.
Tools Used
Manual Review
Hardhat
Recommendations
It is recommended to implement a msg.sender
check on claims:
- function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
+ function claimRewards() external override nonReentrant whenNotPaused returns (uint256) {
- if (user == address(0)) revert InvalidAddress();
+ address user = msg.sender;
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
// Reset user rewards before transfer
userRewards[user] = totalDistributed;
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}