Summary
An user can get reward, by calling getReward()
function.
Contract - BaseGauge.sol
function getReward() external virtual nonReentrant whenNotPaused updateReward(msg.sender) {
if (block.timestamp - lastClaimTime[msg.sender] < MIN_CLAIM_INTERVAL) {
revert ClaimTooFrequent();
}
lastClaimTime[msg.sender] = block.timestamp;
UserState storage state = userStates[msg.sender];
@-> uint256 reward = state.rewards;
if (reward > 0) {
state.rewards = 0;
uint256 balance = rewardToken.balanceOf(address(this));
if (reward > balance) {
revert InsufficientBalance();
}
rewardToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
function flow
1 .getReward()
-> 2 .updateReward(msg.sender)
-> 3 ._updateReward(account)
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}
4 .earned()
-> NON-ZERO
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
5 . getRewardPerToken()
->
NON-ZERO -
function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}
6 .getUserWeight()
->
NON-ZERO -
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
7 . _getBaseWeight(account)
->
NON-ZERO -
function _getBaseWeight(address account) internal view virtual returns (uint256) {
return IGaugeController(controller).getGaugeWeight(address(this));
}
8 . _applyBoost(account, baseWeight);
-> NON-ZERO The only way to return 0 is if veRAAC balance of account is 0.
NOTE: It's possible that veRAAC balance of user is non-zero, but he haven't staked any veRAAC in guage contract.
function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
if (baseWeight == 0) return 0;
IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
uint256 veBalance = veToken.balanceOf(account);
uint256 totalVeSupply = veToken.totalSupply();
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: boostState.totalWeight,
totalVotingPower: boostState.totalVotingPower,
votingPower: boostState.votingPower
});
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
return (baseWeight * boost) / 1e18;
}
since, earned()
is non - zero, so state.rewards
is also non-zero, which means non-zero reward is being tranfered to user.
Vulnerability Details
It means any user who have veRAAC balance, but not staked in gauge can still earn rewards, without staking.
Impact
User who haven't staked veRAAC are also claiming the reward, which is unethical for user who deposited in Gauge contract.
This could possibly drain out whole RAAC balance of Gauge contract.
Tools Used
Manual
Recommendations
Implement a check, to ensure only stakers can claim reward.