veRWA - carrotsmuggler'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: 41/125

Findings: 1

Award: $43.21

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

43.2097 USDC - $43.21

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
upgraded by judge
duplicate-268

External Links

Lines of code

https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L291-L294

Vulnerability details

Impact

Users are allowed to delegate their voting power to other delegates, who will manage them to redirect emissions to certain pools. In order to withdraw the underlying tokens, users can withdraw from the escrow contract once their lock period is over. However, to do this, the need to remove the delegation first.

For users to remove delegated power, their own lock must be longer or equal to the lock of the previous delegated. Users can increase their own lock duration by calling increaseAmount and passing even 1 wei will reset the lock to 5 years. now, users can redelegate tokens to themselves, and then after 5 years, they can withdraw.

The issue arises when the user's lock expires before redelegation. Imagine a user creates a 5 year lock. They then delegate their voting power to some other address. After 5 years, the user's lock has expired. If the user wants to increase their lock time, they call increaseAmount, but this function reverts due to the require statement shown below.

require(locked_.end > block.timestamp, "Lock expired");

So if a user's position is already expired, they cannot increase the duration. If they cannot increase the duration, they cannot redelegate the voting power back to themselves. This is because the toLocked address's lock must not be expired, which in this case is the user themselves.

require(toLocked.end > block.timestamp, "Delegatee lock expired");

Tuhs users cannot re-delegate to themselves. Users are also not able to withdraw, since withdrawing requires voting power to be delegated to themselves.

require(locked_.delegatee == msg.sender, "Lock delegated");

So the user cannot call any function since all of them revert. Thus the user is essentially locked out of the system, losing their tokens forever.

The main issue is that when the contract was forked from the FiatDao contract, the developers merged the increaseUnlockTime and increaseAmount into the same function increaseAmount. The older code in increaseLockTime allowed users to increase their lock times even on expired locks. The increaseAmount function does not let users interact with the function at all. The removal of this function also removed the ability of the users to increase lock duration by any means of already expired positions.

Proof of Concept

A POC has been developed to show the above stated issue. The steps carried out are:

  1. User creates lock of 5 years.
  2. User delegates to a fresh lock after 1 year.
  3. After 4 years, the user's position has expired, but the delgatee still has a live position.
  4. Every function the user can call reverts. This is shown using vm.expectRevert
function testAttack() public {
        // user1 -> victim
        // user2 -> delegatee
        testSuccessCreateLock();
        vm.warp(block.timestamp + 365 days);
        vm.prank(user2);
        ve.createLock{value: LOCK_AMT}(LOCK_AMT);
        vm.prank(user1);
        ve.delegate(user2);
        vm.warp(block.timestamp + 4 * 365 days);
        // Assert user1/victim end timestamp has passed
        assert(block.timestamp > ve.lockEnd(user1));

        // Assert all functions revert
        vm.startPrank(user1);
        vm.expectRevert("Delegatee lock expired");
        ve.delegate(user1);
        vm.expectRevert("Lock delegated");
        ve.withdraw();
        vm.expectRevert("Lock expired");
        ve.increaseAmount{value: 1 ether}(1 ether);
        vm.stopPrank();
    }

This shows that the user is locked out of all operations, and thus loses their tokens.

Tools Used

Foundry

Add the functionality to increase lock time from the FiatDao contract in here. One way can be to implement the function. Another could be to relax the restrictions on the existing increaseAmount function for expired locks since the lock will be updated anyways.

Assessed type

DoS

#0 - c4-pre-sort

2023-08-12T07:34:45Z

141345 marked the issue as duplicate of #223

#1 - c4-pre-sort

2023-08-13T11:59:18Z

141345 marked the issue as not a duplicate

#2 - c4-pre-sort

2023-08-13T11:59:24Z

141345 marked the issue as primary issue

#3 - c4-sponsor

2023-08-16T12:45:39Z

OpenCoreCH marked the issue as sponsor confirmed

#4 - OpenCoreCH

2023-08-21T10:39:06Z

#5 - c4-judge

2023-08-24T07:16:18Z

alcueca 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:49Z

alcueca marked the issue as satisfactory

#8 - c4-pre-sort

2023-08-24T08:20:00Z

141345 marked the issue as not a duplicate

#9 - c4-pre-sort

2023-08-24T08:20:23Z

141345 marked the issue as not a duplicate

#10 - c4-pre-sort

2023-08-24T08:22:05Z

141345 marked the issue as duplicate of #211

#11 - c4-judge

2023-08-26T21:24:29Z

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