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