Summary
The veRAACToken::emergencyWithdraw
and veRAACToken::withdraw
functions allow users to withdraw locked tokens. However, neither function updates the _lockState.totalLocked
variable when a user's lock is removed, leading to incorrect global accounting of locked tokens.
Vulnerability Details
In both veRAACToken::emergencyWithdraw
and veRAACToken::withdraw
, when a user withdraws their locked tokens, their lock entry is deleted from _lockState.locks
, but the _lockState.totalLocked
variable is not decremented. This oversight leads to a situation where totalLocked
remains artificially inflated, causing inconsistencies in subsequent lock operations.
Affected Code in veRAACToken::emergencyWithdraw
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);
}
Affected Code in veRAACToken::withdraw
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 on lock
and increaseLock
Operations
The totalLocked
value is used in lock
and increaseLock
operations to enforce locking limits. Since totalLocked
is not decremented when an emergency withdrawal occurs, new lock attempts will be processed based on incorrect state data.
The LockManager::createLock
is invoked in veRAACToken::lock
function createLock(
LockState storage state,
address user,
uint256 amount,
uint256 duration
) internal returns (uint256 end) {
if (state.minLockDuration != 0 && state.maxLockDuration != 0) {
if (duration < state.minLockDuration || duration > state.maxLockDuration)
revert InvalidLockDuration();
}
if (amount == 0) revert InvalidLockAmount();
end = block.timestamp + duration;
state.locks[user] = Lock({
amount: amount,
end: end,
exists: true
});
state.totalLocked += amount;
emit LockCreated(user, amount, end);
return end;
}
The LockManager::increaseLock
is invoked in veRAACToken::increase
function increaseLock(
LockState storage state,
address user,
uint256 additionalAmount
) internal {
Lock storage lock = state.locks[user];
if (!lock.exists) revert LockNotFound();
if (lock.end <= block.timestamp) revert LockExpired();
if (lock.amount + additionalAmount > state.maxLockAmount) revert AmountExceedsLimit();
lock.amount += additionalAmount;
state.totalLocked += additionalAmount;
emit LockIncreased(user, additionalAmount);
}
Steps to Reproduce
A user locks 100e18
tokens.
The global _lockState.totalLocked
variable increases by 100e18
.
The user calls emergencyWithdraw
, removing their locked tokens but not updating totalLocked
.
When another user tries to lock, totalLocked
reflects an incorrect value, potentially leading to issues in governance and token supply.
Impact
Inconsistent Token Lock Accounting: The protocol tracks an incorrect total locked amount.
Unintended Rejection of Lock Requests: Since totalLocked
is higher than the real locked balance, new locks may be incorrectly blocked.
Tools Used
Manual Review
Recommendations
Update _lockState.totalLocked
When a Lock Is Withdrawn inemergencyWithdraw
to ensure totalLocked
is updated correctly:
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);
// @audit-fix: Properly decrement totalLocked
+ _lockState.totalLocked -= amount;
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}
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);
// @audit-fix: Properly decrement totalLocked
+ _lockState.totalLocked -= amount;
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);
}