Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
Newly created gauge may missed out on its rewards when the first distribute took place, due to the incorrect use of memory variables.
Vulnerability Details
In Voter._distribute(), claimable[_gauge] is assigned to a memory variable _claimable, then claimable[_gauge] is reset to zero.
function_distribute(address_gauge) internal {// Distribute once after epoch has endedrequire( block.timestamp >=IMinter(minter).activePeriod() +IMinter(minter).DURATION(),"can only distribute after period end" );uint256 _claimable = claimable[_gauge];// Reset claimable amount claimable[_gauge] =0;_updateFor(_gauge);if (_claimable >0) {IBaseGauge(_gauge).notifyRewardAmount(_claimable); } ... }
After that claimable[_gauge] is updated in _updateFor(_gauge).
function_updateFor(address_gauge) internal {require(isGauge[_gauge],"invalid gauge");address _pool = poolForGauge[_gauge];uint256 _supplied = weights[_pool];if (_supplied >0) {uint256 _supplyIndex = supplyIndex[_gauge];uint256 _index = index; // get global index0 for accumulated distro supplyIndex[_gauge] = _index; // update _gauge current position to global positionuint256 _delta = _index - _supplyIndex; // see if there is any difference that need to be accruedif (_delta >0) { uint256 _share = (uint256(_supplied) * _delta) / 1e18; // add accrued difference for each supplied token
@> claimable[_gauge] += _share; } } else { supplyIndex[_gauge] = index; } }
At last, if _claimable is greater than zero, reward will be send to gauge.
There is a problem with that _claimable is a memory variable, when claimable[_gauge] be updated, _claimable is not along with it, so there is a scenario that gauge won't receive it's reward on it's first distribute:
1, createGauge() called, gauge A created.
2, some one vote for it.
3, minter called notifyRewardAmount().
4, some one call distribute(), it's the first distribute of gauge A, when contracts runs to distribute(guage A), claimable[_gauge] is greater than zero but _claimable is equal to zero, so gauge A missed out on it's reward this time.
Impact Details
Contracts may not work as intended, in the worst-case scenario, if the distribute() function is called only once, newly created guage could lose its rewards.
// SPDX-License-Identifier: GPL-3pragmasolidity ^0.8.15;import"./BaseTest.sol";import"forge-std/console.sol";contractVotingTest2isBaseTest {functionsetUp() public {setupContracts(block.timestamp); }functiontestNewlyCreatedGauge() public {address[] memory pools =newaddress[](1); pools[0] = sushiPoolAddress;uint256[] memory weights =newuint256[](1); weights[0] =5000; hevm.prank(voter.admin());// Gauge A created. console.log("Gauge A created.");address gauge = voter.createGauge(sushiPoolAddress, IVoter.GaugeType.Passthrough);uint256 tokenId =createVeAlcx(admin, TOKEN_1, MAXTIME,false); hevm.prank(admin);// Some one vote for the pool related to gauge A. console.log("Some one vote for the pool related to gauge A."); voter.vote(tokenId, pools, weights,0);assertEq(alcx.balanceOf(gauge),0);// Give minter some ethers.deal(address(alcx),address(minter), TOKEN_100K); hevm.startPrank(address(minter));require(alcx.approve(address(voter), TOKEN_100K),'approve failed');// Minter call notifyRewardAmount(). console.log("Minter call notifyRewardAmount()."); voter.notifyRewardAmount(TOKEN_100K); hevm.stopPrank();// First distribute of gauge A, no rewards received.// sushiPoolAddress is the reward receiver of gauge A, so we just need to monitor it's alcx balance. console.log("First distribute of gauge A, no rewards received.");uint256 balanceBefore = alcx.balanceOf(sushiPoolAddress); hevm.warp(minter.activePeriod() + minter.DURATION()); voter.distribute();uint256 balanceAfter = alcx.balanceOf(sushiPoolAddress);assertEq(balanceBefore, balanceAfter);// Only by distribute again, gauge A can receive its rewards. console.log("Only by distribute again, gauge A can receive its rewards."); balanceBefore = alcx.balanceOf(sushiPoolAddress); hevm.warp(minter.activePeriod() + minter.DURATION() + minter.DURATION()); voter.distribute(); balanceAfter = alcx.balanceOf(sushiPoolAddress);assertGt(balanceAfter, balanceBefore); }}