Core Contracts

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

Governance checkpoint vulnerability in emergency withdrawals

Description

The emergencyWithdraw() function fails to record a checkpoint when users withdraw their tokens, unlike the regular withdraw() function.

Current implementation of regular withdrawal:

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

Emergency withdrawal lacking checkpoint update:

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];
❌❌ // @audit : no writeCheckpoint() call present here !
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}

Impact

The vulnerability allows users to:

  1. Withdraw tokens through emergency withdrawal

  2. Retain their historical voting power in checkpoint records

  3. Vote on proposals using this phantom voting power

This breaks a fundamental security invariant of the governance system: voting power must be backed by locked tokens.

The vulnerability affects governance functions that check historical voting power:

/**
* @notice Gets voting power at a specific historical block
* @dev Queries the checkpoint history to find the voting power
* @param state The checkpoint state storage
* @param user The user address to query
* @param blockNumber The block number to query
* @return The voting power at the specified block
*/
function getPastVotingPower(
CheckpointState storage state,
address user,
uint256 blockNumber
) internal view returns (uint256) {
if (blockNumber >= block.number) revert InvalidBlockNumber();
return state.userCheckpoints[user].findCheckpoint(blockNumber);
}

This gets called via getVotingPowerForProposal() --> getPastVotes() --> _checkpointState.getPastVotes() --> getPastVotingPower().

Mitigation

Add the call to _checkpointState.writeCheckpoint(msg.sender, 0); inside emergencyWithdraw() before _burn() is called.

Updates

Lead Judging Commences

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

veRAACToken::emergencyWithdraw doesn't update checkpoint - innacurate historical voting power, inconsistent state

Support

FAQs

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