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: 45/125
Findings: 2
Award: $37.43
🌟 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
21.6049 USDC - $21.60
https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L326 https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L356
Lock owner won't be able to undelegate back to himself after his lock has expired, as a result his lock will be permanently stuck in the system and he won't be able to withdraw his funds back.
In order for a user to withdraw his lock, the lock will need to pass three important requirements:
function withdraw() external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.end <= block.timestamp, "Lock not expired"); require(locked_.delegatee == msg.sender, "Lock delegated"); // Update lock uint256 amountToSend = uint256(uint128(locked_.amount)); LockedBalance memory newLocked = _copyLock(locked_); newLocked.amount = 0; newLocked.end = 0; newLocked.delegated -= int128(int256(amountToSend)); newLocked.delegatee = address(0); locked[msg.sender] = newLocked; newLocked.delegated = 0; // oldLocked can have either expired <= timestamp or zero end // currentLock has only 0 end // Both can have >= 0 amount _checkpoint(msg.sender, locked_, newLocked); // Send back deposited tokens (bool success, ) = msg.sender.call{value: amountToSend}(""); require(success, "Failed to send CANTO"); emit Withdraw(msg.sender, amountToSend, LockAction.WITHDRAW, block.timestamp); }
The third require statement in the function withdraw
enforces a user to undelegate his lock in order to withdraw, but currently the system doesn't predict a scenario when a delegated lock expires.
The following issue will occur:
delegate
in order to undelegate the lock back to himself.delegate
, when a user tries to delegate back to himself, the else if statement will be triggered which transfers the current delegatee back to the owner of the lock. In the end the function will revert as the second require statement will be triggered toLocked.end > block.timestamp
indicating that the owner's lock has expired.function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.delegatee != _addr, "Already delegated"); // Update locks int128 value = locked_.amount; address delegatee = locked_.delegatee; LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { // Delegate fromLocked = locked_; toLocked = locked[_addr]; } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; toLocked = locked_; } else { // Re-delegate fromLocked = locked[delegatee]; toLocked = locked[_addr]; // Update owner lock if not involved in delegation locked[msg.sender] = locked_; } require(toLocked.amount > 0, "Delegatee has no lock"); require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); }
Expired delegated locks will be permanently stuck in the system, as neither the lock owner or the delegatee will be able to withdraw the funds:
LockedBalance
even after delegatee withdraws his lock.uint256 amountToSend = uint256(uint128(locked_.amount)); LockedBalance memory newLocked = _copyLock(locked_); newLocked.amount = 0; newLocked.end = 0; newLocked.delegated -= int128(int256(amountToSend)); newLocked.delegatee = address(0); locked[msg.sender] = newLocked; newLocked.delegated = 0;
Manual review
Enforce the require statements to be passed only if the inputted _addr
is not the msg.sender. this indicates that the owner of the lock will either delegate his lock to someone else or the current delegatee will be transferred to another delegatee.
function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.delegatee != _addr, "Already delegated"); // Update locks int128 value = locked_.amount; address delegatee = locked_.delegatee; LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { // Delegate fromLocked = locked_; toLocked = locked[_addr]; } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; toLocked = locked_; } else { // Re-delegate fromLocked = locked[delegatee]; toLocked = locked[_addr]; // Update owner lock if not involved in delegation locked[msg.sender] = locked_; } if (_addr != msg.sender) { require(toLocked.amount > 0, "Delegatee has no lock"); require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); } _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); }
Error
#0 - c4-pre-sort
2023-08-12T11:00:04Z
141345 marked the issue as duplicate of #223
#1 - c4-pre-sort
2023-08-13T12:00:45Z
141345 marked the issue as duplicate of #112
#2 - c4-judge
2023-08-24T07:16:14Z
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:27:37Z
alcueca marked the issue as partial-50
#5 - c4-pre-sort
2023-08-24T08:20:49Z
141345 marked the issue as not a duplicate
#6 - c4-pre-sort
2023-08-24T08:23:03Z
141345 marked the issue as duplicate of #211
#7 - c4-judge
2023-08-24T21:16:00Z
alcueca marked the issue as partial-50
#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
https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L356
Undelegation logic doesn't work as expected, duo to that an owner of a lock would be enforced to reset his lock time for another 5 years through the function increaseAmount
in order to successfully undelegate.
There are few differences between the Voting Escrow of fiat dao and the fork of it made in veRWA.
Duo to this changes in order to undelegate in veRWA, the owner of the lock would need to extend his unlock time for another 5 years, which can be really frustrating if the lock owner wants to undelegate right before the lock end time in order to be able to withdraw.
Explanation of the issue:
delegate
. In the function delegate
the if statement will be triggered which indicates that the owner is going to delegate to someone else. Right before the delegation happens, the function ensures that the delegatee's lock end time is longer than the lock end time of the owner.delegate
in order to undelegate himself. In the function delegate
the else if statement will be triggered which transfers the delegate back to the owner. Before undelegation happens the function enforces that the owner's lock end time is bigger than the end time of the delegatee's lock.increaseAmount
which will reset his lock end time for another 5 years.require(toLocked.end >= fromLocked.end, "Only delegate to longer lock");
In fiat dao, lock owner is able to call the function increaseUnlockTime
and set their lock time just to be over the delegatee's one in order to undelegate. However as there is no such function in veRWA, the only option for a lock owner to undelegate would be to call the function increaseAmount
and reset their unlock time for another 5 years.
function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.delegatee != _addr, "Already delegated"); // Update locks int128 value = locked_.amount; address delegatee = locked_.delegatee; LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { // Delegate fromLocked = locked_; toLocked = locked[_addr]; } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; toLocked = locked_; } else { // Re-delegate fromLocked = locked[delegatee]; toLocked = locked[_addr]; // Update owner lock if not involved in delegation locked[msg.sender] = locked_; } require(toLocked.amount > 0, "Delegatee has no lock"); require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); }
Manual review.
// Same fix as on my other issue
Enforce the require statements to be passed only if the inputted _addr
is not the msg.sender. this indicates that the owner of the lock will either delegate his lock to someone else or the current delegatee will be transferred to another delegatee.
function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.delegatee != _addr, "Already delegated"); // Update locks int128 value = locked_.amount; address delegatee = locked_.delegatee; LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { // Delegate fromLocked = locked_; toLocked = locked[_addr]; } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; toLocked = locked_; } else { // Re-delegate fromLocked = locked[delegatee]; toLocked = locked[_addr]; // Update owner lock if not involved in delegation locked[msg.sender] = locked_; } if (_addr != msg.sender) { require(toLocked.amount > 0, "Delegatee has no lock"); require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); } _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); }
Error
#0 - c4-pre-sort
2023-08-12T10:57:33Z
141345 marked the issue as duplicate of #178
#1 - c4-judge
2023-08-24T07:14:11Z
alcueca marked the issue as duplicate of #82
#2 - c4-judge
2023-08-24T07:20:39Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-08-24T07:23:48Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2023-08-24T07:23:53Z
alcueca marked the issue as partial-50
#5 - c4-judge
2023-08-29T06:37:35Z
alcueca changed the severity to 3 (High Risk)