Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Incorrect accounting in `veRAACToken::emergencyWithdraw` and `veRAACToken::withdraw` due to missing `totalLocked` update

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);
// @audit-issue: `_lockState.totalLocked` is not updated
@> 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);
// Clear lock data
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
// Update checkpoints
_checkpointState.writeCheckpoint(msg.sender, 0);
// Burn veTokens and transfer RAAC
_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; // @audit-issue: totalLocked does not decrease on emergency withdrawal
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();
// Maximum lock amount
if (lock.amount + additionalAmount > state.maxLockAmount) revert AmountExceedsLimit();
lock.amount += additionalAmount;
state.totalLocked += additionalAmount; // @audit-issue: Incorrect if totalLocked is outdated
emit LockIncreased(user, additionalAmount);
}

Steps to Reproduce

  1. A user locks 100e18 tokens.

  2. The global _lockState.totalLocked variable increases by 100e18.

  3. The user calls emergencyWithdraw, removing their locked tokens but not updating totalLocked.

  4. 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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::withdraw / emergencyWithdraw doesn't substract the `_lockState.totalLocked`

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::withdraw / emergencyWithdraw doesn't substract the `_lockState.totalLocked`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.