Core Contracts

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

Attack can Drain All RAACToken in StabilityPool

Severity: High

Likelihood: High


1. Summary

The StabilityPool reward mechanism currently calculates RAACToken rewards based on a user’s entire deposit rather than the withdrawal amount. This oversight enables an attacker to repeatedly execute small withdrawals, each time receiving a reward share computed on the full deposit, ultimately draining the StabilityPool of all RAACToken rewards.


2. Technical Details

2.1. Protocol Context

The protocol is designed to incentivize liquidity providers through a multi-step deposit and reward mechanism:

  • LendingPool Deposit:
    Lenders provide liquidity to the protocol via the LendingPool::deposit function. In return, they receive RToken, which represents their claim on the liquidity pool.

  • StabilityPool Deposit:
    Lenders can then deposit their RToken into the StabilityPool. Upon depositing, they receive a corresponding DEToken (maintained at a strict 1:1 ratio with RToken). The DEToken serves as a receipt for the deposit, keeping track of the user’s share of the pool.

  • Reward Distribution:
    In addition to the token exchange, the StabilityPool distributes rewards in the form of RAACToken. These rewards accumulate over time and are claimable by users when they invoke the StabilityPool::withdraw function.

2.2. Current Reward Calculation Implementation

The function responsible for calculating the RAACToken rewards is implemented as follows:

function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user]; //@audit ENTIRE DEPOSIT IS CONSIDERED
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}
  • Issue: Instead of computing rewards based on the portion of the deposit being withdrawn, the function uses the user's total deposit. This miscalculation allows repeated reward claims based on an inflated deposit value.

2.3. Iterative Attack Scenario

Consider an example with the following initial parameters:

  • Total Deposits in the Pool: 90_000

  • Total RAACToken Rewards in the Pool: 7_000

  • User’s Deposit: 85

A correct full-withdrawal would yield:

Reward = (7000 * 85) / 90000 ≈ 6.61 RAACToken

However, if the attacker withdraws only a small portion at a time, the following sequence can occur:

  1. Iteration 1

    • User’s Deposit (pre-withdrawal): 85

    • Amount Withdrawn: 5

    • Calculated Reward: (7000 * 85) / 90000 ≈ 6.61 RAACToken

    • Remaining Deposit: 80

  2. Iteration 2

    • User’s Deposit (pre-withdrawal): 80

    • Amount Withdrawn: 5

    • Calculated Reward: (7000 * 80) / 90000 ≈ 6.22 RAACToken

    • Remaining Deposit: 75

  3. Iteration 3

    • User’s Deposit (pre-withdrawal): 75

    • Amount Withdrawn: 5

    • Calculated Reward: (7000 * 75) / 90000 ≈ 5.83 RAACToken

    • Remaining Deposit: 70

By repeating these small withdrawals, the attacker consistently receives rewards based on a near-full deposit amount. This iterative process enables them to gradually drain the entire pool of RAACToken rewards claiming more than what would be fair if rewards were tied solely to the withdrawn amount.


3. Impact

  • Reward Draining:
    Attackers can exploit the reward calculation to claim disproportionately high rewards through multiple small withdrawals, effectively draining the StabilityPool’s RAACToken reserves.


4. Tools Used

Manual Review


5. Recommended Fix

To prevent this attack vector, the reward calculation must be modified so that rewards are based on the actual amount being withdrawn, not the full deposit. One potential solution is to update the function signature and logic as follows:

- function calculateRaacRewards(address user) public view returns (uint256) {
+ function calculateRaacRewards(address user, uint256 amountToWithdraw) 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 * amountToWithdraw) / totalDeposits;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.