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: 37/125
Findings: 3
Award: $68.86
π Selected for report: 0
π Solo Findings: 0
π Selected for report: ltyu
Also found by: 0xDING99YA, 3docSec, KmanOfficial, MrPotatoMagic, RED-LOTUS-REACH, Tendency, Yuki, bart1e, bin2chen, carrotsmuggler, cducrest, kaden, mert_eren, pep7siup, popular00, qpzm, seerether, zhaojie
43.2097 USDC - $43.21
https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L326-L349 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L356-L387
When a user create a lock and then delegate the votes to another user, and after the lockEnd time when the user want to withdraw the transaction will revert, there is no way to get the token back and the fund is locked forever.
In withdraw() in VotingEscrow.sol, when withdrawing it requires the delegate is the msg.sender itself:
require(locked_.delegatee == msg.sender, "Lock delegated");
And if a user want to delegate to any user, that user (including the user self) lockEnd must larger than current timestamp:
require(toLocked.end > block.timestamp, "Delegatee lock expired");
So it's possible that user1 delegate to user2, when user1 wants to withdraw after lockEnd, user1 must delegate back to self, however, this is impossible since delegate always check user1.lockEnd > current timestamp.
POC in Foundry is below, just added it to VotingEscrow.t.sol and run:
function testWithdrawRevert() public { // user1 create lock vm.prank(user1); ve.createLock{value: LOCK_AMT}(LOCK_AMT); // user1 lockEnd is 157248000 // make the block.timestamp to 10000000, then user2 create lock vm.warp(10000000); vm.prank(user2); ve.createLock{value: LOCK_AMT}(LOCK_AMT); // user2 lockEnd is 167529600 // user1 delegate to user2 vm.prank(user1); ve.delegate(user2); // make the block.timestamp to 157248001, which is 1s larger than user1.lockEnd vm.warp(157248001); // user1 cannot withdraw directly since delegatee is not the user self vm.expectRevert("Lock delegated"); vm.prank(user1); ve.withdraw(); // user1 can't delegate back to self, but withdraw require(delegatee == msg.sender), so user1 cannot get token back forever vm.expectRevert("Delegatee lock expired"); vm.prank(user1); ve.delegate(user1); }
Manual Review, Foundry
Enable users to delegate back to self even after the lockEnd
Other
#0 - c4-pre-sort
2023-08-12T10:29:03Z
141345 marked the issue as duplicate of #223
#1 - c4-pre-sort
2023-08-13T12:00:38Z
141345 marked the issue as duplicate of #112
#2 - c4-judge
2023-08-24T07:16:03Z
alcueca marked the issue as duplicate of #82
#3 - c4-judge
2023-08-24T07:20:40Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-08-24T07:38:20Z
alcueca marked the issue as satisfactory
#5 - c4-pre-sort
2023-08-24T08:20:04Z
141345 marked the issue as not a duplicate
#6 - c4-pre-sort
2023-08-24T08:20:27Z
141345 marked the issue as not a duplicate
#7 - c4-pre-sort
2023-08-24T08:22:30Z
141345 marked the issue as duplicate of #211
#8 - c4-judge
2023-08-26T21:24:29Z
alcueca changed the severity to 3 (High Risk)
π Selected for report: ADM
Also found by: 0xDING99YA, 3docSec, BenRai, Jorgect, Kow, MrPotatoMagic, QiuhaoLi, RandomUser, SpicyMeatball, Tendency, Topmark, Watermelon, Yanchuan, Yuki, bart1e, cducrest, kaden, lsaudit, nemveer, nonseodion
15.8333 USDC - $15.83
After a user delegate the balance to another user, the user can't delegate back to self unless increase own balance.
In delegate() in VotingEscrow.sol, it requires the lockEnd of new delegatee > lockEnd of current delegatee:
require(toLocked.end > block.timestamp, "Delegatee lock expired");
So if a user delegate balance to another address, to delegate back the user has to call increaseAmount() to update the lockEnd
Manual Review
This is a weird design, not sure the reason to do this. Remove the requirement.
Other
#0 - c4-pre-sort
2023-08-12T10:57:07Z
141345 marked the issue as primary issue
#1 - OpenCoreCH
2023-08-16T14:14:21Z
#2 - c4-sponsor
2023-08-16T14:14:25Z
OpenCoreCH marked the issue as sponsor confirmed
#3 - c4-judge
2023-08-24T07:14:15Z
alcueca marked the issue as duplicate of #82
#4 - c4-judge
2023-08-24T07:20:31Z
alcueca changed the severity to 3 (High Risk)
#5 - c4-judge
2023-08-24T07:20:40Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-24T07:38:08Z
alcueca marked the issue as partial-50
#7 - c4-judge
2023-08-29T06:37:35Z
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
After a user making a deposit, the user will incur an immediate loss in the balance (voting power).
By design, the balance of a user will decrease linearly by time. For example, if a user deposit 1 ether, then the 1 ether balance will decrease by each single second. However, at the same timestamp of depositing the balance should still be 1 ether, this is not true in current implementation.
if (_newLocked.end > block.timestamp && _newLocked.delegated > 0) { userNewPoint.slope = _newLocked.delegated / int128(int256(LOCKTIME)); userNewPoint.bias = userNewPoint.slope * int128(int256(_newLocked.end - block.timestamp)); }
As we can see in _checkPoint(), slope is delegated/LOCKTIME, then current bias is slope*(lockEnd-block.timestamp), the question is when making a deposit the lockEnd is not always block.timestamp+LOCKTIME, it will round to the nearest multiple of WEEK, which can result in a loss in bias and the bias will be less than the current deposit.
Foundry POC is below, just added it to VotingEscrow.t.sol.
function testDepositImmediateLoss() public { // We choose a random block timestamp which is not a multiple of WEEK vm.warp(604799); // user1 make a deposit vm.prank(user1); ve.createLock{value: LOCK_AMT}(LOCK_AMT); // In the same block timestamp user's balance is already less than the value deposited, which is 1 ether uint256 user1Balance = ve.balanceOf(user1); assertLt(user1Balance, 1 ether); }
Manual Review, Foundry
Make sure the user won't loss balance immediately at the same timestamp of deposit.
Other
#0 - 141345
2023-08-13T15:54:15Z
not big round error
QA might be more appropriate.
#1 - OpenCoreCH
2023-08-16T14:49:58Z
Small rounding difference because of epochs, not really a problem in my opinion
#2 - c4-sponsor
2023-08-16T14:50:04Z
OpenCoreCH marked the issue as sponsor acknowledged
#3 - c4-sponsor
2023-08-16T14:50:09Z
OpenCoreCH marked the issue as disagree with severity
#4 - alcueca
2023-08-24T06:13:25Z
The premise for voting is that it is fair, and this vulnerability affects all voters equally. Therefore, I agree with the sponsor with this not being an actual issue, although the mitigation could still be followed for code cleanliness.
#5 - c4-judge
2023-08-24T06:13:30Z
alcueca changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-08-24T06:13:34Z
alcueca marked the issue as grade-a