Core Contracts

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

Treasury Value Conservation Break Undermines RAAC Protocol Economics

Summary

Treasury contract's withdraw function fails to properly maintain value conservation during token withdrawals, and this breaks a fundamental invariant where total value should always match the sum of individual token balances. A manager with proper role access can trigger a withdraw operation that creates a discrepancy between the total value tracking and actual token balances. When the manager withdraws tokens, the contract updates individual token balances but fails to maintain proper synchronization with the total value tracking.

Exploit Path: A manager with MANAGER_ROLE executes a withdrawal

// In Treasury.sol
function withdraw(address token, uint256 amount, address recipient) {
_balances[token] -= amount; // πŸ“Š Updates balance
_totalValue -= amount; // 🏦 Updates total but lacks atomic synchronization
IERC20(token).transfer(recipient, amount);
}

Result

πŸ“Š totalValue β‰  sum of _balances[token]

When managers withdraw tokens, the total value tracking desynchronizes from actual balances, similar to the famous Compound interest calculation bug, but with protocol-wide implications.

Vulnerability Details

The Treasury contract, which acts as the financial backbone of the RAAC protocol. Like a central entity, it manages multiple token types. RAAC governance tokens, RTokens from lending operations, and DETokens from the stability system.

When a manager initiates a withdrawal, the contract attempts to update two critical values. the specific token balance and the total protocol value. There's dangerous desynchronization between these values. Imagine a bank where the main ledger shows a different amount than the sum of all account balances.

The core issue manifests in the withdraw function

function withdraw(
address token,
uint256 amount,
address recipient
) external override nonReentrant onlyRole(MANAGER_ROLE) {
// πŸ” Input validation
if (token == address(0)) revert InvalidAddress();
if (recipient == address(0)) revert InvalidRecipient();
if (_balances[token] < amount) revert InsufficientBalance();
// πŸ’« State updates (critical section)
_balances[token] -= amount; // πŸ“Š Individual balance update
_totalValue -= amount; // 🏦 Global value update
// πŸ’Έ Token transfer
IERC20(token).transfer(recipient, amount);
// πŸ“‘ Event emission
emit Withdrawn(token, amount, recipient);
}

In the state update section where balance and total value updates aren't atomically synchronized. This desynchronization cascades through the entire protocol. The GaugeController relies on accurate Treasury values to calculate boost multipliers for veRAACToken holders. When these values become misaligned, the entire incentive structure warps. A user's boost could be calculated incorrectly, leading to either inflated or diminished rewards.

Impact

This desynchronization could lead to:

  • Incorrect reporting of total protocol value

  • Potential over-withdrawal of funds

  • Breaking of dependent systems that rely on accurate total value tracking

Recommendations

Current Implementation of the withdraw() function

function withdraw(
address token,
uint256 amount,
address recipient
) external override nonReentrant onlyRole(MANAGER_ROLE) {
// πŸ” Input validation
if (token == address(0)) revert InvalidAddress();
if (recipient == address(0)) revert InvalidRecipient();
if (_balances[token] < amount) revert InsufficientBalance();
// πŸ’« State updates (critical section)
_balances[token] -= amount; // πŸ“Š Individual balance update
_totalValue -= amount; // 🏦 Global value update
// πŸ’Έ Token transfer
IERC20(token).transfer(recipient, amount);
// πŸ“‘ Event emission
emit Withdrawn(token, amount, recipient);
}

We require atomic updates with validation, this ensures the Treasury maintains perfect synchronization between individual token balances and total protocol value, preserving the integrity of RAAC's economic mechanisms.

function withdraw(
address token,
uint256 amount,
address recipient
) external override nonReentrant onlyRole(MANAGER_ROLE) {
// πŸ” Input validation
if (token == address(0)) revert InvalidAddress();
if (recipient == address(0)) revert InvalidRecipient();
if (_balances[token] < amount) revert InsufficientBalance();
// πŸ” Capture pre-state
uint256 oldBalance = _balances[token];
uint256 oldTotal = _totalValue;
// πŸ’« State updates (critical section)
_balances[token] = oldBalance - amount; // πŸ“Š Individual balance update
_totalValue = oldTotal - amount; // 🏦 Global value update
// ⚑ Conservation check
require(_totalValue == _calculateTotalBalances(), "Treasury: Value conservation broken");
// πŸ’Έ Token transfer
IERC20(token).transfer(recipient, amount);
// πŸ“‘ Event emission
emit Withdrawn(token, amount, recipient);
}
Updates

Lead Judging Commences

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

Treasury::withdraw doesn't check if one withdraws more than the needed balance for current allocations, doesn't update the _totalValue properly

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

Treasury::withdraw doesn't check if one withdraws more than the needed balance for current allocations, doesn't update the _totalValue properly

Appeal created

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

Support

FAQs

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