Core Contracts

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

Incorrect RAAC Reward Calculation Allows Partial Withdrawals to Claim Full Rewards StabilityPool

Summary

The StabilityPool's reward calculation mechanism contains a critical flaw where users can claim their entire accumulated RAAC rewards while only withdrawing a minimal amount of their deposited funds. This allows malicious actors to drain rewards disproportionately without significantly reducing their deposited capital.

Vulnerability Details

In the withdraw function computes RAAC rewards by calling calculateRaacRewards(msg.sender).

function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
@>> uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}

The calculateRaacRewards functoin computes RAAC rewards based on the entire deposit balance of the user (i.e., userDeposits[user]) rather than proportionally to the withdrawn amount. This allows an attacker to repeatedly withdraw minimal amounts while claiming rewards proportional to their entire remaining deposit, leading to potential drainage of the RAAC token Rewards.

function calculateRaacRewards(address user) public view returns (uint256) {
@>> uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}

POC

  • RAAC Token Balance: 100e18

  • Attacker deposits 1000e18 rToken, receives 1000e18 deToken.

  • Total deToken Supply: 10,000e18

Attack Steps:

  • First Withdrawal:

    • Withdraw 1 wei rToken.

    • Rewards Calculated: (100e18 RAAC * 1000e18 userDeposit) / 10000e18 totalDeposits = 10e18 RAAC.

    • Attacker receives 10e18 RAAC.

    • User deposit reduced to 999.999...e18 rToken.

  • Second Withdrawal :

    • Withdraw 1 wei rToken again.

    • Rewards Calculated: (90e18 RAAC * 999999999999999999999 userDeposit) / 9999999999999999999999 totalDeposits = 8.999999999999999999e18 RAAC.

    • Attacker claims another 8.9e18 RAAC.

Result: By repeating this process, the attacker drains the RAAC Rewards token despite minimal actual withdrawals.

Impact

Malicious users can drain the entire raacToken totalRewards with repeating minimal withdrawal (e.g., 1 wei)

Tools Used

Manual Review

Recommendations

Implement a reward tracking system (e.g., using snapshots or accrued balances) to calculate rewards based on the time-weighted deposit amount.

or

- function calculateRaacRewards(address user) public view returns (uint256) {
+ function calculateRaacRewards(address user, uint256 withdrawAmount) public view returns (uint256) {
- uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
- return (totalRewards * userDeposit) / totalDeposits;
+ return (totalRewards * withdrawAmount) / totalDeposits;
}
Updates

Lead Judging Commences

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

StabilityPool::withdraw can be called with partial amounts, but it always send the full rewards

Support

FAQs

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

Give us feedback!