Open c4-bot-7 opened 1 month ago
alcueca marked the issue as primary issue
Killing gauges can be considered normal operation, therefore the finding and severity are valid.
alcueca marked the issue as satisfactory
alcueca marked the issue as selected for report
Lines of code
https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L239 https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L630-L639
Vulnerability details
Description
The
VoterUpgradeableV2.sol
contract haskillGauge()
that disables the gauge to prevent it from further rewards distribution, only the address with GOVERNANCE_ROLE role can call it. thekillGauge()
only updates three state variablesThe
distribute()
function will distribute rewards to pools managed by theVoterUpgradeableV2.sol
contract and it will call the Minter contract by triggeringupdate_period()
function before distributing rewards.The timeline looks like this
When
distribute()
gets invoked in the timeline it will distribute the rewards of Epoch_x, The killed gauge has no weight in this epoch because its weight gets subtracted fromtotalWeightsPerEpoch[]
inkillGauge()
.When the Minter invokes
VoterUpgradeableV2.sol#notifyRewardAmount()
to notify the contract of the reward amount to be distributed for Epoch_x, we can also find in the same function how theindex
value gets increasedthe
index
is updated as the reward amount divided by the total weights of Epoch_x we know the weight of the disabled gauge is not included intotalWeightsPerEpoch[Epoch_x]
So, back to _distribute()
Because
killGauge()
doesn't delete the values ofweightsPerEpoch[]
, it will send backamount
of emissions back to Minter, which actually should get distributed between the existing poolsTo summarize: the
index
is directly related by the value oftotalWeightsPerEpoch[Epoch_x]
, and thekillGauge()
is subtracted from the weightsPerEpoch of the disabled gauge. so, theindex
didn't include the weight of the killed gauge, but_distribute
calculates its emission and sends it back to Minter.To understand the impact (check the Proof of Concept section for the details) in case the total emissions for Epochx is 80e18 with three active gauges (with the same amount of votes), each pool will receive 26.5e18 token But in case one gauge gets killed one scenario is the 1st gauge will receive 40e18 and the other 40e18 will get transferred back to Minter, this will leave the last gauge with 0 emissions (from here the impact is related to how
gauge.sol#.notifyRewardAmount()
will handle this situation with is out of scope in this contest).Another scenario is to send 40e18 to the two gauges but the disabled gauge gets revived in the next epoch and will be able to receive his 40e18 token because the `gaugesState[gauge].index` is not updated (this will loop us to the above scenario again because the 40e18 tokens do not exist in the first time )
Impact
gaugesState[gauge_].claimable
.distribut()
functionProof of Concept
Let's say now is Epoch_x +1:
index
= 10e18amount_
= 80e18totalWeightsPerEpoch
of Epoch_x is:weightAt
= 1500e18 scenario_01: no gauge gets disabled each gauge will receive26.5e18
tokens as emissionthis is how we calculate it
index += (amount_ 1e18) / weightAt; = (80e18 1e18)/1500e18 = 5.3e16 Now, index = 10.053e18
How
distribute()
calcul theamount
for the 3 pools uint256 delta = index - state.index; = 10.053 e18- 10e18 = 0.053e18uint256 amount = (totalVotesWeight delta) / 1e18; = (500e18 0.053e18)/1e18 = 26.5e18
How
notifyRewardAmount()
increase theindex
uint256 weightAt = 1000e18 uint256 amount_ = 80e18index += (amount_ 1e18) / weightAt; = (80e18 1e18)/1000e18 = 8e16 Now, index = 10.08e18
How
distribute()
calcul theamount
for the 3 pools uint256 delta = index - state.index; = 10.08 e18- 10e18 = 0.08e18uint256 amount = (totalVotesWeight delta) / 1e18; = (500e18 0.08e18)/1e18 = 40e18
However, the fix should take into consideration how the Minter calculates the emissions for every epoch (is it a fixed value every time or depending on how many gauges are active )
Assessed type
Invalid Validation