Core Contracts

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

StabilityPool Withdrawal Precision Break

Summary

StabilityPool's withdrawal mechanism contains a potential accounting error where user balances may not decrease correctly after withdrawal. There's a discrepancy between the expected and actual balance changes. This could impact the protocol's stability and user funds. When a user withdraws funds, the contract should:

  1. Decrease their userDeposits balance by the withdrawn amount

  2. Burn the corresponding deTokens

  3. Transfer the underlying rTokens back to the user

However, the balance decrease may not exactly match the withdrawal amount. This happen due to:

  1. Rounding errors in the exchange rate calculations

  2. Potential interference from the reward distribution mechanism

  3. Missing synchronization between deToken burns and deposit tracking

Vulnerability Details

The StabilityPool serves as the backbone of RAAC's stability mechanism, allowing users to deposit rTokens and earn RAAC rewards while providing liquidation support. However, i come across an edge case in the withdrawal logic.

When Alice deposits 1000 rTokens into the StabilityPool, she receives deTokens representing her share. Later, when attempting to withdraw 500 rTokens, something unexpected happens. The contract's internal accounting shows her balance decreasing by 500, but the actual token movements don't perfectly align, because the contract allows withdrawals where the balance reduction doesn't exactly match the withdrawn amount. This breaks a fundamental accounting principle in RAAC's stability mechanism. Let's see how the withdraw() function works and see the root cause.

function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
// First checkpoint: Update rewards and emission rates
_update(); // → Calls RAACMinter.tick() to update rewards
// Check 1: User must have enough deTokens to burn
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
// Critical Path: Convert deTokens back to rTokens
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount); // → Potential precision loss point
uint256 raacRewards = calculateRaacRewards(msg.sender); // → Reward calculation based on user's share
// Check 2: User must have enough underlying deposits
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
// State Change 1: Reduce user's deposit balance
userDeposits[msg.sender] -= rcrvUSDAmount; // → Key point where balance reduction occurs
// State Change 2: Clean up empty deposits
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender]; // → Complete withdrawal cleanup
}
// Token Operations: Execute the actual token transfers
deToken.burn(msg.sender, deCRVUSDAmount); // → Burns deTokens (DEToken.sol)
rToken.safeTransfer(msg.sender, rcrvUSDAmount); // → Returns original rTokens
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards); // → Distributes RAAC rewards
}
// Event emission for tracking
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}

The vulnerability arises in the interaction between these state changes, particularly in how rcrvUSDAmount relates to deCRVUSDAmount through the exchange rate calculations.

  1. The StabilityPool's deposit tracking

  2. DEToken's supply management

  3. The RAACMinter's reward distribution

This creates a scenario where the mathematical invariant: balanceBefore - balanceAfter == withdrawnAmount doesn't hold true in all cases.

Think of it like an account where your withdrawal amount doesn't exactly match the decrease in your balance, a small discrepancy that could compound over time.

Impact

At its core, the StabilityPool acts as a buffer, allowing users to deposit rTokens (real estate-backed tokens) in exchange for deTokens (deposit tokens) while earning RAAC rewards. This creates a vital stability layer for the protocol's lending operations.

The core mistake emerges in the withdrawal process. Notice how the StabilityPool handles user withdrawals, look at this simplified example.

// The contract assumes a 1:1 relationship between deposits and deTokens
userDeposits[msg.sender] -= amount;
deToken.burn(msg.sender, amount);
}

This means that when Alice deposits 1000 rTokens and later withdraws 500, the contract's accounting can become misaligned with reality.

The misalignment stems from the complex interaction between three core protocol components as i mentioned earlier above.

  • The StabilityPool's deposit tracking

  • The DEToken's supply management

  • The RAACMinter's reward distribution through the dual-gauge system

This creates a ripple effect through RAAC's real estate stability mechanism. When users provide liquidation support or when the protocol needs to maintain its peg, these small discrepancies could amplify, potentially affecting the protocol's ability to maintain stable real estate backing.

Recommendations

Implement precise accounting that maintains the relationship between rTokens and deTokens throughout the protocol's operations, ensuring RAAC's real estate stability mechanism remains robust and trustworthy.

function withdraw(uint256 deCRVUSDAmount) external {
_update(); // Updates RAAC emissions via RAACMinter.tick()
// DEToken.sol interaction: Check deToken balance
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
// Critical Exchange Rate Calculation
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
/* Exchange rate calculation can drift due to:
* 1. Reward distributions affecting total supply
* 2. Rounding in previous operations
* 3. Asynchronous updates between pools
*/
// StabilityPool state update
userDeposits[msg.sender] -= rcrvUSDAmount;
// Token Operations Sequence
deToken.burn(msg.sender, deCRVUSDAmount); // DEToken.sol
rToken.safeTransfer(msg.sender, rcrvUSDAmount); // RToken.sol
// RAAC Rewards Distribution
uint256 raacRewards = calculateRaacRewards(msg.sender);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards); // RAACToken.sol
}
}

The precise implementation would track the relationship

function calculateRcrvUSDAmount(uint256 deCRVUSDAmount) public view returns (uint256) {
uint256 scalingFactor = 10**(18 + rTokenDecimals - deTokenDecimals);
return (deCRVUSDAmount * getExchangeRate()) / scalingFactor;
}

This exchange rate calculation needs to maintain precision through all state changes, particularly during reward distributions and liquidations.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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

Give us feedback!