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: 103/125
Findings: 1
Award: $4.23
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
4.2289 USDC - $4.23
https://github.com/code-423n4/2023-08-verwa/blob/main/src/LendingLedger.sol#L173 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L181 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L69
Within the GaugeController#_get_sum()
, 500
, which is the maximum number of the sum (loop) used in the for-loop, would be too many.
This may lead to that the transaction would be reverted in the for-loop of the GaugeController#_get_sum()
due to reaching the gas limit.
As a result, when a user call the LendingLedger#claim()
, the transaction of the function would also be reverted. Because the GaugeController#_get_sum()
, which is called for calculating the marketWeight
in the LendingLedger#claim()
, would be reverted.
Within the the LendingLedger#claim()
, the GaugeController#gauge_relative_weight_write()
would be called to calculate the marketWeight
like this:
https://github.com/code-423n4/2023-08-verwa/blob/main/src/LendingLedger.sol#L173
/// @notice Claim the CANTO for a given market. Can only be performed for prior (i.e. finished) epochs, not the current one /// @param _market Address of the market /// @param _claimFromTimestamp From which epoch (provided as timestmap) should the claim start. Usually, this parameter should be set to 0, in which case the epoch of the last claim will be used. /// However, it can be useful to skip certain epochs, e.g. when the balance was very low or 0 (after everything was withdrawn) and the gas usage should be reduced. /// Note that all rewards are forfeited forever if epochs are explicitly skipped by providing this parameter /// @param _claimUpToTimestamp Until which epoch (provided as timestamp) should the claim be applied. If it is higher than the timestamp of the previous epoch, this will be used /// Set to type(uint256).max to claim all possible epochs function claim( address _market, uint256 _claimFromTimestamp, uint256 _claimUpToTimestamp ) external is_valid_epoch(_claimFromTimestamp) is_valid_epoch(_claimUpToTimestamp) { address lender = msg.sender; uint256 userLastClaimed = userClaimedEpoch[_market][lender]; require(userLastClaimed > 0, "No deposits for this user"); _checkpoint_lender(_market, lender, _claimUpToTimestamp); _checkpoint_market(_market, _claimUpToTimestamp); uint256 currEpoch = (block.timestamp / WEEK) * WEEK; uint256 claimStart = Math.max(userLastClaimed, _claimFromTimestamp); uint256 claimEnd = Math.min(currEpoch - WEEK, _claimUpToTimestamp); uint256 cantoToSend; if (claimEnd >= claimStart) { // This ensures that we only set userClaimedEpoch when a claim actually happened 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 /// @audit cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); // (marketWeight / 1e18) * (userBalance / marketBalance) * ri.amount; } userClaimedEpoch[_market][lender] = claimEnd + WEEK; } if (cantoToSend > 0) { (bool success, ) = msg.sender.call{value: cantoToSend}(""); require(success, "Failed to send CANTO"); } }
Within the GaugeController#gauge_relative_weight_write()
, the GaugeController#_get_sum()
would be called like this:
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L181
/// @notice Get gauge weight normalized to 1e18 and also fill all the unfilled /// values for type and gauge records /// @dev Any address can call, however nothing is recorded if the values are filled already /// @param _gauge Gauge address /// @param _time Relative weight at the specified timestamp in the past or present /// @return Value of relative weight normalized to 1e18 function gauge_relative_weight_write(address _gauge, uint256 _time) external returns (uint256) { _get_weight(_gauge); _get_sum(); /// @audit return _gauge_relative_weight(_gauge, _time); }
Within the GaugeController#_get_sum()
, the sum of the gauge weights would be calculated by using for-loop, which the maximum number of the sum (loop) is 500
like this:
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L69
/// @notice Fill historic gauge weights week-over-week for missed checkins and return the sum for the future week /// @return Sum of weights function _get_sum() internal returns (uint256) { uint256 t = time_sum; Point memory pt = points_sum[t]; for (uint256 i; i < 500; ++i) { /// @audit if (t > block.timestamp) break; t += WEEK; uint256 d_bias = pt.slope * WEEK; if (pt.bias > d_bias) { pt.bias -= d_bias; uint256 d_slope = changes_sum[t]; pt.slope -= d_slope; } else { pt.bias = 0; pt.slope = 0; } points_sum[t] = pt; if (t > block.timestamp) time_sum = t; } return pt.bias; }
However, within the GaugeController#_get_sum()
, 500
, which is the maximum number of the sum (loop) used in the for-loop, would be too many.
This may lead to that the transaction would be reverted in the for-loop of the GaugeController#_get_sum()
due to reaching the gas limit.
As a result, when a user call the LendingLedger#claim()
, the transaction of the function would also be reverted. Because the GaugeController#_get_sum()
, which is called for calculating the marketWeight
in the LendingLedger#claim()
, would be reverted.
Within the GaugeController#_get_sum()
(GaugeController.sol#L69), consider reducing the maximum number of the sum (loop) in the for-loop from 500
to smaller number.
DoS
#0 - c4-pre-sort
2023-08-12T09:37:21Z
141345 marked the issue as primary issue
#1 - 141345
2023-08-12T09:37:36Z
QA might be more appropriate.
#2 - OpenCoreCH
2023-08-16T14:46:07Z
Dup of https://github.com/code-423n4/2023-08-verwa-findings/issues/384, same comment as there:
The loops are bounded and will never reach the block gas limit, so without a PoC that shows a situation where the limit is reached, this seems invalid to me
#3 - c4-sponsor
2023-08-16T14:46:13Z
OpenCoreCH marked the issue as sponsor disputed
#4 - alcueca
2023-08-24T05:47:14Z
No proof that 500
are too many iterations of the for loop.
The warden should have coded a PoC, but even without it, we know that the function in question reads from memory once and it is feasible to calculate how much gas is used in 500 iterations.
#5 - c4-judge
2023-08-24T05:47:24Z
alcueca marked the issue as unsatisfactory: Insufficient proof
#6 - alcueca
2023-08-28T14:31:41Z
Revisiting this, both _get_sum
and _get_weight
store a Point each per iteration. A Point is 2 words and 5 years of inactivity is about ~250 iterations. In those conditions, a call to claim
would use about 20M gas (20000 (SSTORE) * 2 (Point) * 2 (functions) * 250 (iterations)). The block limit in CANTO is 30M, so it is still not proved that this gas limit could reasonably be reached. It would be worth it to add it to the sponsor assumptions that claim
will be called for each market at least once every five years.
#7 - c4-judge
2023-08-28T14:32:01Z
alcueca changed the severity to QA (Quality Assurance)
#8 - c4-judge
2023-08-28T14:33:25Z
alcueca marked the issue as grade-b