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: 22/125
Findings: 3
Award: $192.42
🌟 Selected for report: 1
🚀 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/GaugeController.sol#L218-L220 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L356
Users can double-spend their voting power, and over vote for their Gauge. This causes unfairness in the distribution of CANTO.
In vote_for_gauge_weights()
of GaugeController.sol, a VoteEscrow locker is able to vote for a gauge. Their voting power is based off of the slope
and bias
of their lock. However, this does not take into account when a voter delegates their voting power to another user. This is problematic because after a user has voted for a gauge, they can delagate their voting power to another user, and therfore, duplicating their vote.
This is a unit test to be added into GaugeController.t.sol. It can be manually run by executing forge test --match-test testUnlockAfterVoted
.
Note: For the sake of brevity, user1
is the gauge address that gets created, it is also a locking user.
function testDelegatedAfterVoted() public { // prepare vm.deal(user1, 100 ether); vm.startPrank(gov); gc.add_gauge(user1); gc.change_gauge_weight(user1, 10000); vm.stopPrank(); uint256 v = 10 ether; // Create an almost empty lock for user2 vm.prank(user2); ve.createLock{value: 1}(1); // Create a lock with 10 ether, and vote 100% vm.startPrank(user1); ve.createLock{value: v}(v); gc.vote_for_gauge_weights(user1, 10000); // Delegate to user2 ve.delegate(user2); vm.stopPrank(); // User2, use user1's delegated votes to vote for gauge vm.prank(user2); gc.vote_for_gauge_weights(user1, 10000); // Gauge weight is 2 ether assertApproxEqRel(gc.get_gauge_weight(user1), 2 * v, 0.1e18); console.log(gc.get_gauge_weight(user1)); }
Manual
Whenever a user delegates their votes, consider,
GaugeController.vote_user_power == 0
.Invalid Validation
#0 - c4-pre-sort
2023-08-12T07:35:12Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:16:50Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:07Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:34:10Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:34:29Z
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:21Z
alcueca marked the issue as satisfactory
🌟 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
56.1726 USDC - $56.17
https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L331 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L371-L374 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L383
In delegate()
of VoteEscrow.sol, a user is able to delegate their locked votes to someone else, and undelegate (i.e. delegate back to themselves). When the user tries to re-delegate, either to someone else or themselves, the lock must not be expired. This is problematic because if a user forgets and lets their lock become expired, they cannot undelegate. This blocks withdrawal, which means their tokens are essentially locked forever.
To exit the system, Alice must call withdraw()
. However, since they've delegated, they will not be able to.
function withdraw() external nonReentrant { ... require(locked_.delegatee == msg.sender, "Lock delegated"); ... }
To re-delegate to themselves (undelegate), they call delegate(alice.address)
. However, there is a check to see if toLocked.end
has expired, which would be true since it would point to Alice's lock.
function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; ... LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { ... // @audit this else if will execute } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; // @audit Delegatee toLocked = locked_; // @audit Alice's lock } ... require(toLocked.end > block.timestamp, "Delegatee lock expired");
This is a test to be added into VoteEscrow.t.sol. It can be manually run by executing forge test --match-test testUnSuccessUnDelegate
.
function testUnSuccessUnDelegate() public { testSuccessDelegate(); vm.warp(ve.LOCKTIME() + 1 days); // Try to undelegate vm.startPrank(user1); vm.expectRevert("Delegatee lock expired"); ve.delegate(user1); // Still user2 (, , , address delegatee) = ve.locked(user1); assertEq(delegatee, user2); }
Manual
Consider refactoring the code to skip toLocked.end > block.timestamp
if undelegating. For example, adding a small delay (e.g., 1 second) to the lock end time when a user undelegates.
Timing
#0 - c4-pre-sort
2023-08-13T01:32:24Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-08-13T01:35:24Z
141345 marked the issue as duplicate of #223
#2 - c4-pre-sort
2023-08-13T12:00:44Z
141345 marked the issue as duplicate of #112
#3 - c4-judge
2023-08-24T07:16:13Z
alcueca marked the issue as duplicate of #82
#4 - c4-judge
2023-08-24T07:20:40Z
alcueca changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-08-24T07:28:15Z
alcueca marked the issue as satisfactory
#6 - c4-pre-sort
2023-08-24T08:20:09Z
141345 marked the issue as not a duplicate
#7 - c4-pre-sort
2023-08-24T08:20:23Z
141345 marked the issue as not a duplicate
#8 - c4-pre-sort
2023-08-24T08:22:58Z
141345 marked the issue as duplicate of #211
#9 - c4-judge
2023-08-25T06:04:30Z
alcueca marked the issue as selected for report
#10 - c4-judge
2023-08-26T21:24:31Z
alcueca changed the severity to 3 (High Risk)
#11 - alcueca
2023-08-26T21:27:04Z
This vulnerability, if not found, would have meant that some users would have permanently lost assets in the for of voting power. While at that point the application owners would certainly warn users to not let their locks expire without undelegating, many users would not get the warning, as it is not that easy to make sure that every user is aware of something. The result is that time and again, users would get their tokens locked forever.
#12 - OpenCoreCH
2023-09-01T09:57:56Z
🌟 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
In remove_gauge()
of GaugeController.sol, Governance is able to remove a gauge. As noted by the Sponsor, Governance is controlled by CANTO token holders. The removal, whether deliberately or unintentionally, will through the usual proposal voting steps. If the gauge is removed, this will result in voters unable to change their existing votes, and losing potential reward claims.
Since vote_for_gauge_weights()
requires that the gauge is valid, a user will not able to call this function again to change their existing vote:
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"); ... }
This is a test to be added into GaugeController.t.sol. It can be manually run by executing forge test --match-test testChangeVoteRemoveGauge
. This shows that a user's vote is locked to a remove gauge.
function testChangeVoteRemoveGauge() public { vm.startPrank(gov); gc.add_gauge(gauge1); gc.add_gauge(gauge2); ve.createLock{value: 1 ether}(1 ether); gc.vote_for_gauge_weights(gauge1, 10000); gc.remove_gauge(gauge1); vm.expectRevert("Invalid gauge address"); gc.vote_for_gauge_weights(gauge1, 0); // Unable to change votes vm.expectRevert("Used too much power"); gc.vote_for_gauge_weights(gauge2, 10000); }
Manual
Consider adding a withdraw existing votes function i.e., remove reliance on having to use vote_for_gauge_weights()
. This function should simply remove a user's existing votes, it does not need to check isValidGauge[_gauge_addr]
.
Governance
#0 - c4-pre-sort
2023-08-12T09:24:13Z
141345 marked the issue as duplicate of #62
#1 - c4-judge
2023-08-25T11:10:42Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-25T22:43:36Z
alcueca changed the severity to 3 (High Risk)