veRWA - ltyu's results

Incentivization Primitive for Real World Assets on Canto

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 22/125

Findings: 3

Award: $192.42

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

36.9443 USDC - $36.94

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-396

External Links

Lines of code

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

Vulnerability details

Impact

Users can double-spend their voting power, and over vote for their Gauge. This causes unfairness in the distribution of CANTO.

Proof of Concept

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));
}

Tools Used

Manual

Whenever a user delegates their votes, consider,

  • Preventing that user from delegating if they've voted for a gauge. This could be as simple as checking GaugeController.vote_user_power == 0.
  • Poking the GaugeController to update the user's vote. The drawback with this approach, is that if a user votes for many gauges, many calls are required.

Assessed type

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

Awards

56.1726 USDC - $56.17

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
upgraded by judge
edited-by-warden
H-04

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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);
}

Tools Used

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.

Assessed type

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

Findings Information

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-62

Awards

99.3104 USDC - $99.31

External Links

Lines of code

https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/GaugeController.sol#L213

Vulnerability details

Impact

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.

Proof of Concept

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); }

Tools Used

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].

Assessed type

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)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter