Vulnerability Details
Function _earned()
:
function _earned(
StakeInfo memory _stakeInfo,
address _account,
uint256 _index
) internal view returns (uint256) {
uint256 vestingRate = _getVestingRate(_stakeInfo);
if (vestingRate == 0) {
return 0;
}
uint256 _perTokenReward;
if (vestingRate == 1e18) {
_perTokenReward = _rewardPerToken();
} else {
_perTokenReward = _rewardPerToken() * vestingRate / 1e18;
}
return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 +
claimableRewards[_account][_index];
}
It can be seen that total earned reward is calculated as (_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 + claimableRewards[_account][_index];
The problem at here is userRewardPerTokenPaid[_account][_index]
is only updated after reward is calculated:
modifier updateReward(address _account, uint256 _index) {
{
rewardData.rewardPerTokenStored = uint216(_rewardPerToken());
rewardData.lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData.periodFinish));
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(_stakeInfo, _account, _index);
userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18;
}
}
_;
}
Which mean at the first time a account is called in that index, userRewardPerTokenPaid[_account][_index]
will equal to 0. Which open scenario that attacker will flashloan to stake and withdraw to able to claim reward
Impact
User without any contribution can get reward
Tools Used
MAnual review
Recommendations