Open code423n4 opened 1 year ago
pt.slope -= d_slope underflow, DoS gauge operation.
OpenCoreCH marked the issue as sponsor confirmed
alcueca marked the issue as primary issue
alcueca marked the issue as selected for report
This finding does a great job at describing the vulnerability and its impact from a computational point of view, including an executable PoC. It's duplicate (#386) is also worthy of note since it explains the root cause from a mathematical point of view. Although this finding was selected as best, both findings should be read for their complementary points of view.
Lines of code
https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L91-L114 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L142 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L180 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L189 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L247
Vulnerability details
_get_weight
function is used in order to return the total gauge's weight and it also updates past values of thepoints_weight
mapping, iftime_weight[_gauge_addr]
is less or equal to theblock.timestamp
. It contains the following loop:There are two possible scenarios:
pt.bias > d_bias
pt.bias <= d_bias
The first scenario will always happen naturally, since
pt.bias
will be the total voting power allocated for some point and since slope is a sum of all users' slopes and slopes are calculated in such a way that<SLOPE> * <TIME_TO_END_OF_STAKING_PERIOD> = <INITIAL_BIAS>
.However, it is possible to artificially change
points_weight[_gauge_addr][t].bias
by callingchange_gauge_weight
(which can be only called by the governance). It important to notice here, thatchange_gauge_weight
doesn't modifypoints_weight[_gauge_addr][t].slope
change_gauge_weight
does permit to change the weight to a smaller number than its current value, so it's both perfectly legal and possible that governance does this at some point (it could be changing the weight to0
or any other value smaller than the current one).Then, at some point when
_get_weight
is called, we will enter theelse
block becausept.bias
will be less than the sum of all user's biases (since originally these values were equal, butpt.bias
was lowered by the governance). It will setpt.bias
andpt.slope
to0
.After some time, the governance may realise that the gauge's weight is
0
, but should be bigger and may change it to some bigger value.We will have the situation where
points_weight[_gauge_addr][t].slope = 0
andpoints_weight[_gauge_addr][t].bias > 0
.If this happens and there is any nonzero
changes_weight[_gauge_addr]
not yet taken into account (for instance in the week after the governance update), then all the functions related to the gauge at_gauge_addr
will not work.It's because, the following functions:
checkpoint_gauge
gauge_relative_weight_write
gauge_relative_weight
_change_gauge_weight
change_gauge_weight
vote_for_gauge_weights
remove_gauge
call
_get_weight
at some point.Let's see what will happen in
_get_weight
when it's called:We will enter the
if
statement, becausept.bias
will be> 0
andpt.slope
will be0
(or some small value, if users give their voting power to gauge in the meantime), since it was previously set to0
in theelse
statement and wasn't touched when gauge's weight was changed by the governance. We will:d_bias
frompt.bias
which will succeedchanges_weight[_gauge_addr][t]
fromd_slope
However, there could be a user (or users) whose voting power allocation finishes at
t
for somet
not yet handled. It means thatchanges_weight[_gauge_addr][t] > 0
(and ifpt.slope
is not0
, thenchanges_weight[_gauge_addr][t]
still may be greater than it).If this happens, then the integer underflow will happen in
pt.slope -= d_slope;
. It will now happen in every call to_get_weight
and it won't be possible to recover, because:vote_for_gauge_weights
will revertchange_gauge_weight
will revertas they call
_get_weight
internally. So, it won't be possible to modifypt.slope
andpt.bias
for any point in time, so therevert
will always happen for that gauge. It won't even be possible to remove that gauge.So, in short, the scenario is as follows:
X
.X
.X
drops to0
.X
since it wants to incentivise users to provide liquidity inX
._get_weight
attempts to subtractchanges_weight[_gauge_addr][t]
from the current slope (which is either0
or some small value) and it results in integer underflow.X
is unusable and it's impossible to withdraw voting power from (so users cannot give their voting power somewhere else). The weight ofX
cannot be changed anymore andX
cannot be even removed.Note that it is also possible to frontrun the call to
change_gauge_weight
when the weight is set to a lower value - user with a lot of capital can watch the mempool and if weight is lowered to some valuex
, he can give a voting power ofx
to that gauge. Then, right after weight is changed by the governance, he can withdraw his voting power, leaving the gauge with weight =0
. Then, governance will manually increase the weight to recover and DoS will happen as described. So it is only needed that governance decreases gauge's weight at some point.Impact
As stated, above the impact is that the entire gauge is useless, voting powers are permanently locked there and its weight is impossible to change, so the impact is high.
In order for this situation to succeed, governance has to decrease weight of some gauge, but I think it's very likely, because:
_get_weight
checks thatif (pt.bias > d_bias)
and it handles the opposite situation, so it is anticipated that it may genuinely happen.remove_gauge
).old_bias
is greater thanold_sum_bias + new_bias
is handled invote_for_gauge_weights
, but it may only happen when gauge's weight was decreased by the governance.old_slope.slope
is greater thanold_sum_slope + new_slope.slope
is also handled there, but it may only happen if we enter theelse
statement in_get_weight
.So, it is predicted that gauge's weight may be lowered and the protocol does its best to handle it properly, but as I showed, it fails to do so. Hence, I believe that this finding is of High severity, because although it requires governance to perform some action (decrease weight of some gauge), I believe that it's likely that governance decides to decrease weight, especially that it is anticipated in the code and edge cases are handled there (and they wouldn't be if we assumed that governance would never allowed them to happen).
Proof of Concept
Please run the test below. The test shows slightly simplified situation where governance just sets weight to
0
forgauge1
, but as I've described above, it suffices that it's just changed to a smaller value and it may drop to0
naturally as users withdraw their voting power. The following import will also have to be added:import {Test, stdError} from "forge-std/Test.sol";
.Tools Used
VS Code
Recommended Mitigation Steps
Perform
pt.slope -= d_slope
in_get_weight
only whenpt.slope >= d.slope
and otherwise zero it out.Assessed type
Under/Overflow