Summary
Contract - BaseGauge.sol
function_flow
when user claims his reward.
getReward()
-> updateReward()
-> _updateReward()
-> earned()
-> getRewardPerToken()
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);
}
}
modifier updateReward(address account) {
_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);
}
}
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
@-> (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}
Vulnerability Details
the return value in getRewardPerToken()
is -
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
-
This can be problematic.
-
Suppose a malicious user leveraging flashloan, deposits very high amount of staking tokens via stake()
function, just before a claimer is claiming his reward. (basically frontrunning)
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
@-> _totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
This will increase _totalSupply
to very large value, means totalSupply()
will return very large value as :-
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
-
subsequently, (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
will become very less.
-
Leading to loss of reward claim value to claimer.
-
One above operations done, attacker can withdraw staking tokens by calling withdraw
function, replaying the flashloan amount.
Impact
Tools Used
Manual
Recommendations
Implement a time delay mechanism between stake
and withdraw
function. if tha attacker wants to perform this attack it will fail, bacause of time delay between stake and withdraw function.