Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: high
Invalid

Malicious User Can Grief Protocol by Repeatedly Calling `CommunityVCS::claimRewards`, Preventing Rewards from Accruing

Summary

In the Chainlink RewardVault, a malicious user can exploit the CommunityVCS::claimRewards function to continuously claim rewards on behalf of any vault, even with minimal accrued rewards. This prevents the rewards from compounding, disrupting the proper accrual process for legitimate users and vaults, which relies on the accumulation of unclaimed rewards.

Vulnerability Details

The claimReward() function in the Chainlink RewardVault contract allows users' rewards to compound over time. As unclaimed rewards accrue, they grow based on the staker’s principal and the reward per token, including a multiplier effect. The greater the accrued rewards, the larger the multiplier applied, resulting in a greater overall reward when eventually claimed.

function claimReward() external whenNotPaused returns (uint256) {
// @audit Determine if the caller is an operator.
bool isOperator = _isOperator(msg.sender);
// @audit Update the reward per token for the caller's pool.
_updateRewardPerToken(isOperator ? StakerType.OPERATOR : StakerType.COMMUNITY);
// @audit Get the correct staking pool for the caller.
IStakingPool stakingPool =
isOperator ? IStakingPool(i_operatorStakingPool) : IStakingPool(i_communityStakingPool);
// @audit Retrieve the caller's staked amount.
uint256 stakerPrincipal = _getStakerPrincipal(msg.sender, stakingPool);
// @audit Calculate the caller's rewards.
StakerReward memory stakerReward = _calculateStakerReward({
staker: msg.sender,
isOperator: isOperator,
stakerPrincipal: stakerPrincipal
});
// @audit Calculate new vested rewards with MULTIPLIER.
uint112 newVestedBaseRewards = _calculateNewVestedBaseRewards(
stakerReward, _getMultiplier(_getStakerStakedAtTime(msg.sender, stakingPool))
);
// @audit Update the staker's reward balances.
stakerReward.unvestedBaseReward -= newVestedBaseRewards;
stakerReward.claimedBaseRewardsInPeriod += newVestedBaseRewards;
// @audit Calculate total vested rewards.
uint256 newVestedRewards = stakerReward.vestedBaseReward + newVestedBaseRewards;
delete stakerReward.vestedBaseReward;
// @audit Include delegated rewards for operators.
if (isOperator) {
newVestedRewards += stakerReward.vestedDelegatedReward;
delete stakerReward.vestedDelegatedReward;
}
// @audit Ensure there are rewards to claim.
if (newVestedRewards == 0) {
revert NoRewardToClaim();
}
// @audit Update the staker's reward record.
s_rewards[msg.sender] = stakerReward;
// @audit Transfer the rewards to the caller.
// The return value is not checked since the call will revert if any balance, allowance or
// receiver conditions fail.
i_LINK.transfer(msg.sender, newVestedRewards);
// @audit Log the reward claim and update.
emit RewardClaimed(msg.sender, newVestedRewards);
emit StakerRewardUpdated(
msg.sender,
0,
0,
stakerReward.baseRewardPerToken,
stakerReward.operatorDelegatedRewardPerToken,
stakerReward.claimedBaseRewardsInPeriod
);
// @audit Return the amount of rewards claimed.
return newVestedRewards;
}

The above is the implement of the claimReward() function in Chainlink RewardVault contract with "@audit" comments explaining every code line. Take note of the multiplier effect. The smaller the accrued reward been multiplied, the smaller the overall claimed reward.

However, in the CommunityVCS::claimRewards function, a malicious user can repeatedly call the function with an arbitrary _minRewards parameter, triggering the claim of rewards on behalf of any vault, regardless of how small the accrued reward is. The relevant code is as follows:

function claimRewards(
uint256[] calldata _vaults,
uint256 _minRewards
) external returns (uint256) {
address receiver = address(this);
uint256 balanceBefore = token.balanceOf(address(this));
for (uint256 i = 0; i < _vaults.length; ++i) {
ICommunityVault(address(vaults[_vaults[i]])).claimRewards(_minRewards, receiver);
}
uint256 balanceAfter = token.balanceOf(address(this));
return balanceAfter - balanceBefore;
}

This enables an attacker, even one not participating in the StakeLink protocol, to grief the system by repeatedly claiming small amounts of rewards. As a result, vaults lose the benefit of compounded rewards, reducing the potential payouts for legitimate users.

Impact

This attack prevents vaults from accruing rewards over time, leading to significantly smaller reward claims due to the reduced multiplier effect. The protocol and its users are left vulnerable to this griefing attack, where an external actor can repeatedly claim small amounts of rewards, effectively preventing meaningful accrual.

Tools Used

Manual

Recommendations

Introduce a minimum claim threshold or restrict the ability to call CommunityVCS::claimRewards to legitimate protocol participants. Alternatively, impose limits on the frequency of reward claims or ensure the rewards accrued reach a meaningful amount before allowing a claim to be processed.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.