Summary
In VeRAACToken.sol, there is an emergency function emergencyWithdraw()
that can be activated to allow users to bypass their withdrawal duration and withdraw their tokens.
In this function, users will burn their veRAACTokens and get back RAACTokens, and their lock position will be deleted. However, their checkpoints will not be deleted, as it should be when removing their lock position, similar to unlock()
.
Vulnerability Details
In veRAACToken.emergencyWithdraw()
, 4 things happen, _lockState
is deleted, _votingState
is deleted, _burn()
is called on their veRAACToken and raacToken
is transferred to the caller,
function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert NoTokensLocked();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
> delete _lockState.locks[msg.sender];
> delete _votingState.points[msg.sender];
> _burn(msg.sender, currentPower);
> raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}
This process should be similar to withdraw()
since they are both removing locks and burning veRAAC for RAAC tokens, but it misses calling _checkpointState()
.
This is the withdraw() function where 5 things happen during a withdrawal process.
function withdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert LockNotFound();
if (block.timestamp < userLock.end) revert LockNotExpired();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
> delete _lockState.locks[msg.sender];
> delete _votingState.points[msg.sender];
> _checkpointState.writeCheckpoint(msg.sender, 0);
> _burn(msg.sender, currentPower);
> raacToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
Impact
High Impact, Low likelihood for an emergency
The checkpointState is not updated for the user. The issue is that checkpointState()
is used to query past votes, which is misleading and may lead the protocol to think that the user still has votes.
* @notice Gets the historical voting power for an account at a specific block
* @dev Returns the voting power from the checkpoint at or before the requested block
* @param account The address to check voting power for
* @param blockNumber The block number to check voting power at
* @return The voting power the account had at the specified block
*/
function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
> return _checkpointState.getPastVotes(account, blockNumber);
}
Tools Used
Manual Review
Recommendations
Call _checkpointState.writeCheckpoint(msg.sender, 0);
in emergencyWithdraw()
as it is done in withdraw()
.