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: 36/125
Findings: 2
Award: $74.88
🌟 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/9a2e7be003bc1a77b3b87db31f3d5a1bcb48ed32/src/VotingEscrow.sol#L383 https://github.com/code-423n4/2023-08-verwa/blob/9a2e7be003bc1a77b3b87db31f3d5a1bcb48ed32/src/VotingEscrow.sol#L331 https://github.com/code-423n4/2023-08-verwa/blob/9a2e7be003bc1a77b3b87db31f3d5a1bcb48ed32/src/VotingEscrow.sol#L294
The validation checks that VotingEscrow
has in place around delegation prevent some invalid cases from happening. However, the combination of these checks happens to prevent the withdrawal of funds that reached maturity while delegated.
Users who delegated their funds to a delegatee, and let their own lock come to expiration, will see their funds permanently locked in the VotingEscrow contract.
The following Foundry test shows the point:
function testPocFundsLockedInDelegation() public { uint256 currentTs = block.timestamp; // Alice locks some CANTO vm.prank(alice); ve.createLock{value: 10 ether}(10 ether); // Bob locks some CANTO vm.prank(bob); ve.createLock{value: 10 wei}(10 wei); // Alice delegates Bob vm.startPrank(alice); ve.delegate(bob); // Both Alice's and Bob's veCANTO reach maturity vm.warp(currentTs + LOCKTIME); // Alice can't withdraw because the delegation is in place ... vm.expectRevert("Lock delegated"); ve.withdraw(); // ... and she can't undo the delegation because her own lock is expired ... vm.expectRevert("Delegatee lock expired"); ve.delegate(alice); // ... and she can't renew the lock vm.expectRevert("Lock expired"); ve.increaseAmount{value: 1 wei}(1 wei); // Alice is in a cul-de-sac 😰 }
Code review, Foundry
Relax the validation steps, possibly removing the "Delegatee lock expired" when delegating to self:
// Then, update delegatee's lock and voting power (checkpoint) locked_ = locked[delegatee]; require(locked_.amount > 0, "Delegatee has no lock"); - require(locked_.end > block.timestamp, "Delegatee lock expired"); + require(locked_.end > block.timestamp || _addr == msg.sender, "Delegatee lock expired"); newLocked = _copyLock(locked_); newLocked.delegated += int128(int256(_value)); locked[delegatee] = newLocked;
Other
#0 - c4-pre-sort
2023-08-11T11:55:06Z
141345 marked the issue as duplicate of #223
#1 - c4-pre-sort
2023-08-13T12:00:23Z
141345 marked the issue as duplicate of #112
#2 - c4-judge
2023-08-24T07:15:56Z
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:41:35Z
alcueca marked the issue as satisfactory
#5 - c4-pre-sort
2023-08-24T08:19:58Z
141345 marked the issue as not a duplicate
#6 - c4-pre-sort
2023-08-24T08:20:26Z
141345 marked the issue as not a duplicate
#7 - c4-pre-sort
2023-08-24T08:21:59Z
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
31.6666 USDC - $31.67
VotingEscrow has one check in place for avoiding vote manipulation: delegation can move funds only from a shorter to a longer-locking account, and this includes when the delegation is undone. This is the root cause of an issue that is below.
This presents the scenario of Alice locking her 1M CANTO in VotingEscrow, and delegating to Bob because she is not very into keeping an eye on the votings happening.
Bob, on the contrary, is likely to be an entity who uses VotingEscrow just for receiving delegations of many users and voting on their behalf. Both of these motivations offer Bob a concrete incentive in keeping their lock as far in time as possible (i.e. keeping it at 5 years by increasing their funds by 1 wei per week), because:
When Bob acts this way (which, as said, seems the most reasonable way to act for Bob), Alice may:
To withdraw the funds, Alice has only one option:
The situation can be showed in the following Foundry test:
function testWithdrawAfterDelegation() public { uint256 currentTs = block.timestamp; vm.prank(alice); ve.createLock{value: 10 ether}(10 ether); vm.prank(bob); ve.createLock{value: 10 wei}(10 wei); vm.prank(alice); ve.delegate(bob); vm.warp(currentTs + LOCKTIME - 10 days); vm.prank(bob); ve.increaseAmount{value: 10 wei}(10 wei); // Alice can't withdraw vm.startPrank(alice); vm.expectRevert("Only delegate to longer lock"); ve.delegate(alice); // The only option to get the CANTO back is to lock for 5 more years ve.increaseAmount{value: 10 wei}(10 wei); ve.delegate(alice); vm.warp(currentTs + 2 * LOCKTIME - 20 days); ve.withdraw(); }
Code review, Foundry
There is, unfortunately, no easy fix I can think of to this issue because simply removing the "Only delegate to longer lock" check, even only for un-delegating, can easily open the possibility for bypassing the desired vote power decay over time.
Maybe it could be worth considering a change in the design:
Doing so will make the voting power of each delegator decay according to the original (delegator's) lock, so funds can be withdrawn when the original lock expires without opening the possibility of voting manipulation.
Governance
#0 - c4-pre-sort
2023-08-12T07:41:08Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-08-13T14:35:12Z
141345 marked the issue as duplicate of #82
#2 - c4-pre-sort
2023-08-13T14:35:17Z
141345 marked the issue as not a duplicate
#3 - c4-pre-sort
2023-08-13T14:35:25Z
141345 marked the issue as duplicate of #178
#4 - c4-pre-sort
2023-08-14T09:51:40Z
141345 marked the issue as not a duplicate
#5 - c4-pre-sort
2023-08-14T09:52:03Z
141345 marked the issue as duplicate of #82
#6 - c4-judge
2023-08-24T07:20:40Z
alcueca changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-08-24T07:40:20Z
alcueca marked the issue as satisfactory
#8 - c4-pre-sort
2023-08-24T08:21:48Z
141345 marked the issue as not a duplicate
#9 - c4-pre-sort
2023-08-24T08:24:23Z
141345 marked the issue as duplicate of #375
#10 - c4-judge
2023-08-29T06:37:00Z
alcueca marked the issue as duplicate of #182
#11 - c4-judge
2023-08-29T06:37:36Z
alcueca changed the severity to 3 (High Risk)