killGauge()
will lead to wrong calculation of emission
#8
Labels
3 (High Risk)
Assets can be stolen/lost/compromised directly
bug
Something isn't working
H-01
primary issue
Highest quality submission among a set of duplicates
🤖_04_group
AI based duplicate group recommendation
satisfactory
satisfies C4 submission criteria; eligible for awards
selected for report
This submission will be included/highlighted in the audit report
sponsor confirmed
Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
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.the
killGauge()
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_xwe know the weight of the disabled gauge is not included in
totalWeightsPerEpoch[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 Epoch_x 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
.The impact depends on the order of the gauges array that passed to
distribut()
functionProof of Concept
Let's say now is Epoch_x +1:
index
= 10e18amount_
= 80e18totalWeightsPerEpoch
of Epoch_x is:weightAt
= 1500e18scenario_01: no gauge gets disabled
each gauge will receive
26.5e18
tokens as emissionthis is how we calculate it
scenario_02: one gauge gets disabled
so the
totalWeightsPerEpoch
of Epoch_x now is :weightAt
= 1000e18With the current logic, two gauges each will receive
40e18
tokens as emission +40e18
should be sent back to Minter, which is larger than the total emission which is80e18
this is how we calculate it
Tools Used
Manual Review
Recommended Mitigation Steps
One fix is to delete the
weightsPerEpoch[][]
inkillGauge()
function killGauge(address gauge_) external onlyRole(_GOVERNANCE_ROLE) { ... uint256 epochTimestamp = _epochTimestamp(); totalWeightsPerEpoch[epochTimestamp] -= weightsPerEpoch[epochTimestamp][state.pool]; + delete weightsPerEpoch[epochTimestamp][state.pool]; emit GaugeKilled(gauge_); }
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
The text was updated successfully, but these errors were encountered: