Platform: Code4rena
Start Date: 07/08/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 125
Period: 3 days
Judge: alcueca
Total Solo HM: 4
Id: 274
League: ETH
Rank: 30/125
Findings: 2
Award: $115.78
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: deadrxsezzz
Also found by: 0xComfyCat, Brenzee, Team_Rocket, Yanchuan, auditsea, bin2chen, cducrest, markus_ether, oakcobalt
105.9553 USDC - $105.96
The issue is applied differently based on how change_gauge_weight
works.
An attacker can front-run change_gauge_weight
transaction to manipulate slope
which can result in reverts in the future in getting the gauge weight
Vote does not have effect before change_gauge_weight
is called once.
function _get_weight(address _gauge_addr) private returns (uint256) { uint256 t = time_weight[_gauge_addr]; if (t > 0) { // time_weight[_gauge_addr] is updated here ... } else { return 0; } }
In _get_weight
function, time_weight is updated to next_time
when original value is set.
The time_weight
for a gauge is initialized only through change_gauge_weight
.
So when vote_for_gauge_weights
is called before time_weight
is initialized, _get_weight
always return zero and time_weight
also remains zero.
Thus, updating points_weight
has no effect but changes_weight
has effect.
function _get_weight(address _gauge_addr) private returns (uint256) { uint256 t = time_weight[_gauge_addr]; if (t > 0) { ... if (pt.bias > d_bias) { pt.bias -= d_bias; uint256 d_slope = changes_weight[_gauge_addr][t]; pt.slope -= d_slope; // <-- where issue happens } else { pt.bias = 0; pt.slope = 0; } ... } else { return 0; } }
Assuming there are some vote transactions that are front-run before change_gauge_weight
is called, at some point in the future, _get_weight
can be reverted on the line mentioned above.
Because, points_weight
is not changed so pt.bias > d_bias
will be true, but inside it, pt.slope
will be smaller than d_slope
since there had been changes made to changes_weight
.
This will cause revert, once this is reverted, the GaugeController
will not work anymore.
Manual Review
There can be more than one ways to resolve this issue as follows:
add_gauge
is called. (as in remove_gauge)function add_gauge(address _gauge) external onlyGovernance { // @audit-ok require(!isValidGauge[_gauge], "Gauge already exists"); _change_gauge_weight(_gauge, 0); isValidGauge[_gauge] = true; emit NewGauge(_gauge); }
vote_for_gauge_weights
, set time_weight
to next_timepoints_weight[_gauge_addr][next_time].bias = Math.max(old_weight_bias + new_bias, old_bias) - old_bias; points_sum[next_time].bias = Math.max(old_sum_bias + new_bias, old_bias) - old_bias; time_weight[_gauge_addr] = next_time; // <-- add this line
if (pt.bias > d_bias) { pt.bias -= d_bias; uint256 d_slope = changes_weight[_gauge_addr][t]; // update like this if (pt.slope > d_slope) { pt.slope -= d_slope; } else { pt.slope = 0; } } else { pt.bias = 0; pt.slope = 0; }
Appling one of these would resolve the issue, but appling all of them would make it perfect!
DoS
#0 - c4-pre-sort
2023-08-13T07:04:44Z
141345 marked the issue as primary issue
#1 - OpenCoreCH
2023-08-16T13:20:59Z
Dup of https://github.com/code-423n4/2023-08-verwa-findings/issues/288 (missing time_weight
initialization)
#2 - c4-sponsor
2023-08-16T13:21:06Z
OpenCoreCH marked the issue as sponsor confirmed
#3 - c4-judge
2023-08-25T10:28:16Z
alcueca marked the issue as duplicate of #288
#4 - c4-judge
2023-08-25T10:31:10Z
alcueca marked the issue as not a duplicate
#5 - thebrittfactor
2023-08-25T15:00:50Z
For transparency, the judge requested staff via private message to add the duplicate-288
label back to this submission.
#6 - c4-judge
2023-08-25T20:06:52Z
alcueca marked the issue as partial-50
#7 - c4-judge
2023-08-25T22:44:44Z
alcueca changed the severity to 2 (Med Risk)
#8 - c4-judge
2023-08-25T22:45:02Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: RED-LOTUS-REACH
Also found by: 0x3b, 0x4non, 0xCiphky, 0xDING99YA, 0xDetermination, 0xE1, 0xG0P1, 0xStalin, 0xWaitress, 0xbrett8571, 0xhacksmithh, 0xkazim, 0xmuxyz, 0xweb3boy, 14si2o_Flint, AlexCzm, Alhakista, Bube, Bughunter101, Deekshith99, Eeyore, Giorgio, HChang26, InAllHonesty, JP_Courses, KmanOfficial, MatricksDeCoder, Mike_Bello90, MrPotatoMagic, Naubit, QiuhaoLi, RHaO-sec, Raihan, Rolezn, SUPERMAN_I4G, Shubham, Silverskrrrt, Strausses, T1MOH, Topmark, Tripathi, Watermelon, _eperezok, aakansha, auditsea, audityourcontracts, ayden, carlos__alegre, castle_chain, cducrest, ch0bu, d23e, deadrxsezzz, deth, devival, erebus, fatherOfBlocks, halden, hassan-truscova, hpsb, hunter_w3b, imkapadia, immeas, jat, kaden, kaveyjoe, klau5, koxuan, kutugu, ladboy233, lanrebayode77, leasowillow, lsaudit, markus_ether, matrix_0wl, merlin, nemveer, ni8mare, nonseodion, oakcobalt, owadez, p_crypt0, pipidu83, piyushshukla, popular00, ppetrov, rjs, sandy, sl1, supervrijdag, tay054, thekmj, wahedtalash77, windhustler, zhaojie
9.8204 USDC - $9.82
LendingLedger -> claim
function iterates through weeks and claim rewards per week for the user.
However it will revert when market balance is zero for a specific epoch, which will not let users claim rewards.
Imagine of the short period when USDC price depegged. In that case, users will try to withdraw their money from markets, which might cause zero balance for the market. When USDC price is balanced again, users would try to deposit again to the market. So there can be specific epoches where market balance is zero.
for (uint256 i = claimStart; i <= claimEnd; i += WEEK) { uint256 userBalance = lendingMarketBalances[_market][lender][i]; uint256 marketBalance = lendingMarketTotalBalance[_market][i]; RewardInformation memory ri = rewardInformation[i]; require(ri.set, "Reward not set yet"); // Can only claim for epochs where rewards are set, even if it is set to 0 uint256 marketWeight = gaugeController.gauge_relative_weight_write(_market, i); // Normalized to 1e18 cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); // (marketWeight / 1e18) * (userBalance / marketBalance) * ri.amount; }
However in the codebase, it's not checking if marketBalance is zero, which means that zero maket balance at a specific epoch will cause revert claiming rewards.
Manual Review
for (uint256 i = claimStart; i <= claimEnd; i += WEEK) { uint256 userBalance = lendingMarketBalances[_market][lender][i]; uint256 marketBalance = lendingMarketTotalBalance[_market][i]; if (marketBalance == 0) continue; RewardInformation memory ri = rewardInformation[i]; require(ri.set, "Reward not set yet"); // Can only claim for epochs where rewards are set, even if it is set to 0 uint256 marketWeight = gaugeController.gauge_relative_weight_write(_market, i); // Normalized to 1e18 cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); // (marketWeight / 1e18) * (userBalance / marketBalance) * ri.amount; }
Continue loop when market balance is zero will resolve it.
Math
#0 - c4-pre-sort
2023-08-13T05:22:17Z
141345 marked the issue as duplicate of #394
#1 - c4-judge
2023-08-24T21:23:21Z
alcueca changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-08-25T22:50:09Z
alcueca marked the issue as grade-a