Core Contracts

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

Premature withdrawals could destabilize the protocol's governance and economic incentives that carefully balance real estate and DeFi interests.

Summary

veRAACToken withdrawal validation can be bypassed when emergency withdrawal is disabled and the lock hasn't expired. This creates a vulnerability in the time-lock mechanism that protects locked RAAC tokens. The veRAACToken.sol implementation shows the withdrawal logic: function 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 and transfer tokens
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);
}

The issue is that the withdrawal function properly validates lock expiry but doesn't handle the emergency withdrawal state correctly, this creates a scenario where users could potentially withdraw tokens even when emergency withdrawals are disabled and their lock hasn't expired.

Vulnerability Details

We expects withdrawals to fail when locks haven't expired and emergency withdrawals are disabled. This makes perfect sense, because we want to protect the protocol's vote-escrow mechanism that powers the dual-gauge system for real estate and DeFi directionality.

But currently the implementation in veRAACToken.sol handles lock expiry checks but misses a crucial validation.

/**
* @notice Withdraws locked RAAC tokens after lock expiry
* FLOW: Lock → Vote → Wait → Withdraw
* PROTECTION: Time-lock + Emergency Controls
*/
function withdraw() external nonReentrant {
// S1: Load user's lock position
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
// VALIDATION LAYER
if (userLock.amount == 0) revert LockNotFound(); // Protection: No empty withdrawals
if (block.timestamp < userLock.end) revert LockNotExpired(); // @audit: Missing emergency state check
// S2: Prepare withdrawal amounts
uint256 amount = userLock.amount; // Original RAAC tokens locked
uint256 currentPower = balanceOf(msg.sender); // Current voting power
// S3: Clean protocol state
delete _lockState.locks[msg.sender]; // Clear lock data
delete _votingState.points[msg.sender]; // Remove voting power
// S4: Update governance tracking
_checkpointState.writeCheckpoint(msg.sender, 0); // Reset checkpoint
// S5: Execute token operations
_burn(msg.sender, currentPower); // Remove veRAAC tokens
raacToken.safeTransfer(msg.sender, amount); // Return original RAAC
emit Withdrawn(msg.sender, amount);
}
  1. The function handles conversion: veRAAC (locked) → RAAC (liquid)

  2. Critical validation happens early

  3. State changes follow clear progression:

    • Load → Validate → Calculate → Clean → Update → Transfer

  4. The vulnerability point: Lock expiry check doesn't consider emergency state

  5. Impact flows through governance (voting power) and economics (token transfers)

As can be seen, this structure shows how the withdrawal mechanism integrates with RAAC's broader governance and tokenomics systems.

This means that a user could withdraw tokens even when emergency withdrawals are disabled and their lock hasn't expired. For a protocol bridging real estate and DeFi, this undermines the core stability mechanism. Think of this like breaking a certificate of deposit (CD) before maturity without paying the early withdrawal penalty.

Impact

This vulnerability could undermine the entire vote-escrow tokenomics by allowing premature withdrawals. The time-lock mechanism is crucial for maintaining long-term protocol alignment through veRAACToken voting power.

Tools Used

manual

Recommendations

Add explicit emergency withdrawal validation in the withdraw function

/**
* @notice Withdraws locked RAAC tokens after lock expiry
* FLOW: Lock → Vote → Wait → Withdraw
* PROTECTION LAYERS: Time-lock + Emergency Controls
*/
function withdraw() external nonReentrant {
// Load user's lock position
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
// ENHANCED VALIDATION LAYER
// First check: Verify lock exists
if (userLock.amount == 0) revert LockNotFound();
// Recommended Fix: Withdrawal validation
// Condition 1: Emergency withdrawals disabled (emergencyWithdrawDelay == 0)
// Condition 2: Lock hasn't expired (block.timestamp < userLock.end)
// Both must be false to allow withdrawal
if (emergencyWithdrawDelay == 0 && block.timestamp < userLock.end)
revert LockNotExpired();
// Prepare withdrawal amounts
uint256 amount = userLock.amount; // Original RAAC tokens locked
uint256 currentPower = balanceOf(msg.sender); // Current voting power
// Clean protocol state
delete _lockState.locks[msg.sender]; // Clear lock data
delete _votingState.points[msg.sender]; // Remove voting power
// Update governance tracking
_checkpointState.writeCheckpoint(msg.sender, 0); // Reset checkpoint
// Execute token operations
_burn(msg.sender, currentPower); // Remove veRAAC tokens
raacToken.safeTransfer(msg.sender, amount); // Return original RAAC
emit Withdrawn(msg.sender, amount);
}

Withdrawal allowed only if:

  • Emergency withdrawals are enabled OR

  • Lock period has expired

State Transitions

Initial → Validation → State Cleanup → Token Operations

Security Layers

nonReentrant → Lock Exists → Time/Emergency Check → State Updates → Transfers
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!