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: 3/125
Findings: 5
Award: $2,554.76
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: 0x73696d616f
Also found by: 0xCiphky, 0xComfyCat, 0xDetermination, GREY-HAWK-REACH, QiuhaoLi, SpicyMeatball, Team_Rocket, Tricko, Yanchuan, deadrxsezzz, immeas, kaden, lanrebayode77, ltyu, mert_eren, nonseodion, oakcobalt, popular00, th13vn
36.9443 USDC - $36.94
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L211 https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L356
Users can get infinite voting power by abusing delegate
function.
Upon voting for a gauge in GaugeController
a call is made to VotingEscrow
to get the user's voting power.The problem is that the user's voting power can decrease in two ways:
Upon such activity, user's voting towards a gauge is not adjusted. A group of X Users can easily abuse this by doing the following:
This way a group of X people can increase their voting power X times (Meaning a group of 10 people can use this to get 10X the voting power they're supposed to have). By doing this back and forth between numerous wallets, this could be done to infinity.
Here's a simple PoC of two users doing this. At the end there's additional lines showcasing what their max voting power is supposed to be.
function testDelegateIssue() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 100); vm.stopPrank(); vm.prank(user1); ve.createLock{value: 1 ether}(1 ether); vm.startPrank(user2); ve.createLock{value: 1 ether}(1 ether); ve.delegate(user1); console.log("user 2 delegates to user 1"); vm.stopPrank(); vm.startPrank(user1); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("weight after first deposit", weight); console.log("user 1 delegates to user 2"); ve.delegate(user2); vm.stopPrank(); vm.startPrank(user2); console.log("user2 delegates back to themselves"); ve.delegate(user2); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); weight = gc.get_gauge_weight(gauge1); console.log("weight after second deposit", weight); vm.startPrank(user1); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); vm.prank(user2); gc.vote_for_gauge_weights(gauge1, 10000); weight = gc.get_gauge_weight(gauge1); console.log("what the max voting power for user1 and user2 is supposed to be", weight); }
and the results:
[PASS] testDelegateIssue() (gas: 1875508) Logs: user 2 delegates to user 1 weight after first deposit 1986849314989257700 user 1 delegates to user 2 user2 delegates back to themselves weight after second deposit 3973698629978515300 what the max voting power for user1 and user2 is supposed to be 1986849314989257700
Manual review
Upon reducing a user's voting power, a call to GaugeController must be made.
Other
#0 - c4-pre-sort
2023-08-11T13:31:59Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T12:37:33Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-08-13T12:37:47Z
141345 marked the issue as primary issue
#3 - c4-sponsor
2023-08-16T12:44:54Z
OpenCoreCH marked the issue as sponsor confirmed
#4 - alcueca
2023-08-25T10:51:14Z
Downgrading to Medium and back to High to set all duplicates to High
#5 - c4-judge
2023-08-25T10:51:24Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-25T10:51:51Z
alcueca changed the severity to 3 (High Risk)
#7 - c4-judge
2023-08-25T10:55:39Z
alcueca marked the issue as selected for report
#8 - c4-judge
2023-08-25T10:58:21Z
alcueca marked issue #396 as primary and marked this issue as a duplicate of 396
#9 - c4-judge
2023-08-25T22:00:04Z
alcueca marked the issue as satisfactory
🌟 Selected for report: deadrxsezzz
Also found by: 0xComfyCat, Brenzee, Team_Rocket, Yanchuan, auditsea, bin2chen, cducrest, markus_ether, oakcobalt
275.4837 USDC - $275.48
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L118 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L204
Voting power towards gauges will be lost and project will not work properly
The mapping time_weight
takes a gauge as a param and returns the most recent timestamp a gauge has had its weight recorded/ updated. There are 2 ways to set this value: through _get_weight
and _change_gauge_weight
.
function _get_weight(address _gauge_addr) private returns (uint256) { uint256 t = time_weight[_gauge_addr]; if (t > 0) { Point memory pt = points_weight[_gauge_addr][t]; for (uint256 i; i < 500; ++i) { 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_weight[_gauge_addr][t]; pt.slope -= d_slope; } else { pt.bias = 0; pt.slope = 0; } points_weight[_gauge_addr][t] = pt; if (t > block.timestamp) time_weight[_gauge_addr] = t; } return pt.bias; } else { return 0; } }
The problem in _get_weight
is that the initial value of any time_weight[_gauge_addr]
will be 0. It will go through the entirety of the loop and t
will increase +1 week for every iteration. The problem is that even after 500 iterations t
will be < block.timestamp
so the value of time_weight[_gauge_addr]
will remain 0. Unless admins call manually _change_gauge_weight
to set an initial value, time_weight[_gauge_addr]
will remain 0. Any time a user will use _get_weight
to fill with recent data, the function will iterate over old values and will do nothing. Recent values won't be set and anything depending on it will receive 0 as a recent value.
function _change_gauge_weight(address _gauge, uint256 _weight) internal { uint256 old_gauge_weight = _get_weight(_gauge); uint256 old_sum = _get_sum(); uint256 next_time = ((block.timestamp + WEEK) / WEEK) * WEEK; points_weight[_gauge][next_time].bias = _weight; time_weight[_gauge] = next_time; uint256 new_sum = old_sum + _weight - old_gauge_weight; points_sum[next_time].bias = new_sum; time_sum = next_time; }
Since _change_gauge_weight
is not called within add_gauge
, even if we expect the owners to call it, any votes happening in the time between the adding of the gauge and the admin set function will be lost. The user will only be able to retrieve them by later removing their vote and voting again.
Here are 3 written test-cases which prove the statements above:
function testWithoutManualSet() public { vm.startPrank(gov); gc.add_gauge(gauge1); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting: ", weight); vm.stopPrank(); } function testWithManualSet() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 0); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting: ", weight); vm.stopPrank(); } function testWithChangeMidway() public { vm.startPrank(gov); gc.add_gauge(gauge1); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting: ", weight); vm.stopPrank(); vm.prank(gov); gc.change_gauge_weight(gauge1, 0); vm.startPrank(user1); gc.vote_for_gauge_weights(gauge1, 10000); weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting after admin set", weight); gc.vote_for_gauge_weights(gauge1, 0); gc.vote_for_gauge_weights(gauge1, 10000); weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting after admin set after vote reset", weight); }
and the respective results:
[PASS] testWithoutManualSet() (gas: 645984) Logs: gauge's weight after voting: 0
[PASS] testWithManualSet() (gas: 667994) Logs: gauge's weight after voting: 993424657416307200
[PASS] testWithChangeMidway() (gas: 744022) Logs: gauge's weight after voting: 0 gauge's weight after voting after admin set 0 gauge's weight after voting after admin set after vote reset 993424657416307200
Manual Review, Foundry
Upon adding a gauge, make a call to change_gauge_weight
and set its initial weight to 0.
Error
#0 - c4-pre-sort
2023-08-12T14:51:04Z
141345 marked the issue as primary issue
#1 - 141345
2023-08-15T02:44:33Z
forget to initialize time_weight[]
when add new gauge.
#2 - c4-sponsor
2023-08-16T13:08:28Z
OpenCoreCH marked the issue as sponsor confirmed
#3 - OpenCoreCH
2023-08-21T09:56:38Z
#4 - c4-judge
2023-08-25T10:27:16Z
alcueca marked the issue as duplicate of #401
#5 - c4-judge
2023-08-25T10:27:31Z
alcueca marked the issue as not a duplicate
#6 - c4-judge
2023-08-25T10:29:48Z
alcueca marked the issue as satisfactory
#7 - c4-judge
2023-08-25T10:29:54Z
alcueca marked the issue as selected for report
#8 - c4-judge
2023-08-25T10:48:49Z
alcueca marked the issue as primary issue
#9 - alcueca
2023-08-25T22:44:38Z
Cycling between severities to change all duplicates to High
#10 - c4-judge
2023-08-25T22:44:46Z
alcueca changed the severity to 2 (Med Risk)
#11 - c4-judge
2023-08-25T22:45:06Z
alcueca changed the severity to 3 (High Risk)
#12 - JeffCX
2023-08-28T20:23:56Z
While I respect judge's expertise, I think the severity is medium instead of high
admin can just set initial value after deploy
https://docs.code4rena.com/awarding/judging-criteria/severity-categorization
2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
there is no asset that can be compromised directly
and the pre-condition is the admin make mistake and does not set initial value.
#13 - deadrosesxyz
2023-08-28T20:48:07Z
Contrary to warden's comment:
The wrong voting accounting will result into wrong distribution of rewards, including some users receiving absolutely 0 (full loss of rewards). Wrong voting accounting in a voting system is enough to classify an issue as High Severity.
Also there is no admin mistake as a pre-condition. It is not expected by the admin to set an initial value.
🌟 Selected for report: thekmj
Also found by: 0xCiphky, 0xDetermination, 0xbrett8571, Eeyore, Team_Rocket, Tripathi, bart1e, deadrxsezzz, immeas, ltyu, mert_eren, pep7siup, popular00
99.3104 USDC - $99.31
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L211
Users will forever lose their voting power.
In the case where a gauge is removed, any user that has put voting power into it will forever lose it, as they'll be unable to remove their voting power from the gauge, due to the following lines of code:
function vote_for_gauge_weights(address _gauge_addr, uint256 _user_weight) external { require(_user_weight >= 0 && _user_weight <= 10_000, "Invalid user weight"); require(isValidGauge[_gauge_addr], "Invalid gauge address");
Since isValidGauge[_gauge_addr]
will return false (since gauge is now removed), users will be unable to remove their voting power from said gauge.
Removing a gauge wouldn't be uncommon and it cannot be expected that all users will have removed their voting power prior to the removal, hence the High severity of the issue.
Here's a simple PoC showcasing the issue:
function stuckVotingPower() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 100); gc.add_gauge(gauge2); gc.change_gauge_weight(gauge2, 100); vm.stopPrank(); vm.startPrank(user1); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); vm.prank(gov); gc.remove_gauge(gauge1); vm.startPrank(user1); vm.expectRevert("Invalid gauge address"); gc.vote_for_gauge_weights(gauge1, 0); vm.expectRevert("Used too much power"); gc.vote_for_gauge_weights(gauge2, 500); }
Manual review
Allow for users to undelegate their voting power even if gauge is removed. Consider something changing the faulty require statement to:
require((isValidGauge[_gauge_addr] || _user_weight == 0), "Invalid gauge address"); ## Assessed type Access Control
#0 - c4-pre-sort
2023-08-12T07:00:27Z
141345 marked the issue as duplicate of #62
#1 - c4-judge
2023-08-25T11:11:09Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-25T22:43:22Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-08-25T22:43:36Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: deadrxsezzz
2133.2145 USDC - $2,133.21
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L204
Users can get extra voting power by front-running calls to change_gauge_weight
It can be expected that in some cases calls will be made to change_gauge_weight
to increase or decrease a gauge's weight. The problem is users can be monitoring the mempool expecting such calls. Upon seeing such, any people who have voted for said gauge can just remove their vote prior to change_gauge_weight
. Once it executes, they can vote again for their gauge, increasing its weight more than it was expected to be:
Example:
change_gauge_weight
with value 15_000.Gauge weight was supposed to be changed from 10_000 to 15_000, but due to the user front-running, gauge weight is now 25_000
Manual review
Instead of having a set function, use increase/ decrease methods.
Other
#0 - c4-pre-sort
2023-08-13T07:05:32Z
141345 marked the issue as duplicate of #401
#1 - c4-judge
2023-08-25T10:28:10Z
alcueca marked the issue as duplicate of #288
#2 - c4-judge
2023-08-25T10:35:27Z
alcueca marked the issue as partial-50
#3 - c4-judge
2023-08-25T22:45:04Z
alcueca changed the severity to 3 (High Risk)
#4 - c4-judge
2023-08-28T21:33:45Z
alcueca marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2023-08-28T21:33:50Z
alcueca marked the issue as not a duplicate
#6 - alcueca
2023-08-28T21:34:50Z
Votes are only counted at the first second of an epoch, front-running change_gauge_weight
won't do anything.
#7 - deadrosesxyz
2023-08-29T11:43:58Z
Hey, I'm leaving a simple PoC showcasing the issue and the respective logs from it:
function testWithFrontRun() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 0); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting: ", weight); vm.stopPrank(); vm.prank(user1); gc.vote_for_gauge_weights(gauge1, 0); // front-run transaction by user vm.prank(gov); gc.change_gauge_weight(gauge1, 1000000000000000000); vm.startPrank(user1); gc.vote_for_gauge_weights(gauge1, 10000); // back-run weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after changing weight: ", weight); vm.stopPrank(); } function testWithoutFrontRun() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 0); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); uint weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after voting: ", weight); vm.stopPrank(); vm.prank(gov); gc.change_gauge_weight(gauge1, 1000000000000000000); weight = gc.get_gauge_weight(gauge1); console.log("gauge's weight after government changes weight: ", weight); }
[PASS] testWithFrontRun() (gas: 702874) Logs: gauge's weight after voting: 993424657416307200 gauge's weight after changing weight: 1993424657416307200
[PASS] testWithoutFrontRun() (gas: 674264) Logs: gauge's weight after voting: 993424657416307200 gauge's weight after government changes weight: 1000000000000000000
#8 - c4-judge
2023-08-30T06:29:12Z
alcueca changed the severity to 2 (Med Risk)
#9 - c4-judge
2023-08-30T06:29:17Z
alcueca marked the issue as satisfactory
#10 - alcueca
2023-08-30T06:29:51Z
From the sponsor:
In practice the function should only be used to set the weights to 0 (e.g., when a market got exploited and not all users withdrew their votes yet). But there is nothing that prevents it from setting it to a higher or lower value (at least not in the audited codebase, added that in the meantime). So what is described in the finding is generally true, although it does not require frontrunning or anything like that, because the function just changes the current voting result, anyway. So if voting for the market is not restricted afterwards (by removing it from the whitelist), people can continue to vote on it and the final vote result may be different than what governance has set with this function.
#11 - alcueca
2023-08-30T06:33:03Z
From what I understand the function is essentially broken, and a Medium severity is appropriate given that wardens can't assume that it will be used only to set the weights to 0 for non-whitelisted markets.
#12 - OpenCoreCH
2023-08-30T08:49:09Z
🌟 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
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L118 https://github.com/code-423n4/2023-08-verwa/blob/main/src/LendingLedger.sol#L129
Lost and unclaimable rewards
Upon adding a gauge to gaugeController
users can vote for it to give it weight to get a part of the rewards distributed for the week. It can be expected that upon adding a gauge to gaugeController
users will start voting for it. However, in order to be eligible for a part of the rewards, users have to enter a market which is synced to the LendingLedger
.
The problem is that in order for the market to be synced with LendingLedger
, it has to first be whitelisted.
function sync_ledger(address _lender, int256 _delta) external { address lendingMarket = msg.sender; require(lendingMarketWhitelist[lendingMarket], "Market not whitelisted");
In the case where a gauge is added to gaugeController
and votes are spent towards it, but the epoch ends before it is whitelisted in LendingLedger
, so users can deposit towards the market, the gauge will have a cut of the rewards, but no one will be able to claim it, leading to stuck and lost rewards.
Manual review
Add an external call in GaugeController
which upon adding a gauge, whitelists it in LendingLedger
Error
#0 - 141345
2023-08-12T14:48:30Z
privileged role function sequence
QA might be more appropriate.
#1 - c4-sponsor
2023-08-16T14:58:41Z
OpenCoreCH marked the issue as sponsor acknowledged
#2 - OpenCoreCH
2023-08-16T14:59:00Z
It is governance's responsibility to whitelist it in both systems (at the same time).
#3 - c4-judge
2023-08-24T21:28:05Z
alcueca changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-08-24T21:28:09Z
alcueca marked the issue as grade-a